20 gru 2025·6 min czytania

Wzorzec outbox w PostgreSQL dla niezawodnych integracji API

Poznaj wzorzec outbox: zapisuj zdarzenia w PostgreSQL, a następnie dostarczaj je do zewnętrznych API z ponownymi próbami, zachowaniem kolejności i deduplikacją.

Wzorzec outbox w PostgreSQL dla niezawodnych integracji API

Dlaczego integracje zawodzą, nawet gdy Twoja aplikacja działa

Często zdarza się, że w aplikacji widzisz „powodzenie”, a integracja za kulisami zawodzi. Zapis do bazy jest szybki i niezawodny. Wywołanie zewnętrznego API — niekoniecznie. Powstają więc dwa różne światy: Twój system mówi, że zmiana zaszła, a zewnętrzny system o tym nie wie.

Typowy przykład: klient składa zamówienie, aplikacja zapisuje je w PostgreSQL, a potem próbuje powiadomić przewoźnika. Jeśli dostawca czasu oczekiwania przekroczy 20 sekund i Twoje wywołanie się podda, zamówienie nadal istnieje, ale wysyłka nigdy nie została utworzona.

Użytkownicy odbierają to jako zachowanie mylące lub niespójne. Brakujące zdarzenia wyglądają jak „nic się nie stało”. Podwójne zdarzenia — jak „dlaczego naliczono mi opłatę dwa razy?”. Zespoły wsparcia też mają trudności, bo trudno stwierdzić, czy błąd leży po stronie aplikacji, sieci czy partnera.

Retrye pomagają, ale same w sobie nie gwarantują poprawności. Jeśli powtórzysz wywołanie po timeoutcie, możesz wysłać to samo zdarzenie dwukrotnie, bo nie wiesz, czy partner otrzymał pierwsze żądanie. Jeśli retryy odbywają się poza kolejnością, możesz wysłać „Order shipped” zanim „Order paid”.

Te problemy zazwyczaj wynikają z normalnej współbieżności: wielu workerów pracujących równolegle, wielu serwerów aplikacyjnych zapisujących w tym samym czasie oraz kolejek „best effort”, których timing zmienia się przy obciążeniu. Tryby awarii są przewidywalne: API padają lub zwalniają, sieci gubią żądania, procesy padają w nieodpowiednim momencie, a retrye tworzą duplikaty, kiedy nic nie wymusza idempotencji.

Wzorzec outbox istnieje, bo takie awarie są normalne.

Czym jest wzorzec outbox w prostych słowach

Wzorzec outbox jest prosty: gdy Twoja aplikacja dokonuje ważnej zmiany (np. tworzy zamówienie), w tej samej transakcji zapisuje też mały rekord „zdarzenie do wysłania” w tabeli bazy danych. Jeśli commit bazy się powiedzie, wiesz, że zarówno dane biznesowe, jak i rekord zdarzenia istnieją razem.

Następnie oddzielny worker czyta tabelę outbox i dostarcza te zdarzenia do zewnętrznych API. Jeśli API jest wolne, niedostępne lub wystąpi timeout, główne żądanie użytkownika i tak się zakończy, bo nie czeka na wywołanie zewnętrzne.

To unika niezręcznych stanów, które powstają przy wywoływaniu API wewnątrz handlera żądań:

  • Zamówienie jest zapisane, ale wywołanie API zawiodło.
  • Wywołanie API powiodło się, ale Twoja aplikacja padła przed zapisaniem zamówienia.
  • Użytkownik próbuje ponownie i wysyłasz to samo dwa razy.

Wzorzec outbox głównie pomaga przy zagubionych zdarzeniach, awariach częściowych (baza ok, API nie ok), przypadkowych podwójnych wysyłkach i bezpieczniejszych retryach (możesz spróbować później bez zgadywania).

Nie naprawi wszystkiego. Jeśli ładunek jest błędny, zasady biznesowe są nieprawidłowe lub API odrzuca dane, wciąż potrzebujesz walidacji, dobrej obsługi błędów i sposobu na inspekcję oraz naprawę nieudanych zdarzeń.

Projektowanie tabeli outbox w PostgreSQL

Dobra tabela outbox jest nudna z premedytacją. Powinna być łatwa do zapisu, łatwa do odczytu i trudna do niewłaściwego użycia.

Oto praktyczny szkic schematu, który możesz dostosować:

create table outbox_events (
  id            bigserial primary key,
  aggregate_id  text not null,
  event_type    text not null,
  payload       jsonb not null,
  status        text not null default 'pending',
  created_at    timestamptz not null default now(),
  available_at  timestamptz not null default now(),
  attempts      int not null default 0,
  locked_at     timestamptz,
  locked_by     text,
  meta          jsonb not null default '{}'::jsonb
);

Wybór ID

Użycie bigserial (lub bigint) utrzymuje sortowanie proste i indeksy szybkie. UUIDy są świetne do unikalności między systemami, ale nie sortują się według czasu utworzenia, co może utrudnić przewidywalne polling i obciążyć indeksy.

Częsty kompromis: zachowaj id jako bigint dla kolejności i dodaj osobne event_uuid, jeśli potrzebujesz stabilnego identyfikatora do udostępniania między usługami.

Indeksy, które mają znaczenie

Twój worker będzie wykonywał te same zapytania cały dzień. Większość systemów potrzebuje:

  • Indeksu typu (status, available_at, id) do pobierania kolejnych oczekujących zdarzeń w kolejności.
  • Indeksu na (locked_at) jeśli planujesz wygaszać przeterminowane blokady.
  • Indeksu typu (aggregate_id, id) jeśli czasami dostarczasz zdarzenia per agregat w kolejności.

Utrzymuj ładunki stabilne

Trzymaj payloady małe i przewidywalne. Przechowuj to, czego odbiorca faktycznie potrzebuje, nie cały wiersz. Dodaj wyraźną wersję (np. w meta), aby móc bezpiecznie ewoluować pola.

Używaj meta do routingu i kontekstu debugowania, jak tenant ID, correlation ID, trace ID i dedup key. Ten dodatkowy kontekst zapłaci się później, gdy wsparcie będzie musiało odpowiedzieć „co się stało z tym zamówieniem?”.

Jak bezpiecznie zapisać zdarzenia razem ze zmianą biznesową

Najważniejsza zasada jest prosta: zapisz dane biznesowe i wpis outbox w tej samej transakcji bazy danych. Jeśli transakcja się commitnie, oba istnieją. Jeśli się wycofa, żadne nie istnieje.

Przykład: klient składa zamówienie. W jednej transakcji wstawiasz wiersz zamówienia, pozycje zamówienia i jeden wiersz outbox typu order.created. Jeśli któryś krok się nie powiedzie, nie chcesz, aby zdarzenie „created” wyciekło na zewnątrz.

Jedno zdarzenie czy wiele?

Zacznij od jednego zdarzenia na akcję biznesową, kiedy możesz. Łatwiej to zrozumieć i taniej przetwarzać. Dziel na wiele zdarzeń tylko wtedy, gdy różni konsumenci naprawdę potrzebują innego czasu dostarczenia lub innego payloadu (np. order.created dla fulfilment i payment.requested dla billing). Generowanie wielu zdarzeń dla jednego kliknięcia zwiększa retrye, problemy z kolejnością i obsługą duplikatów.

Jaki payload przechowywać?

Zwykle wybierasz między:

  • Snapshotem: przechowaj kluczowe pola tak, jak były w momencie akcji (suma zamówienia, waluta, ID klienta). To unika dodatkowych odczytów później i utrzymuje wiadomość stabilną.
  • Referencją: przechowaj tylko ID zamówienia i pozwól workerowi pobrać szczegóły później. To zmniejsza rozmiar outbox, ale dodaje odczyty i może się zmienić, jeśli zamówienie zostanie edytowane.

Praktyczny kompromis to identyfikatory plus mały snapshot krytycznych wartości. Pomaga to odbiorcom działać szybko i ułatwia debugowanie.

Trzymaj granicę transakcji ciasno. Nie wywołuj zewnętrznych API w tej samej transakcji.

Dostarczanie zdarzeń do zewnętrznych API: pętla workera

Przekształć przepływ pracy w oprogramowanie
Generuj backend, interfejs webowy i aplikacje mobilne z jednego projektu z czytelnym kodem źródłowym.
Zbuduj aplikację

Gdy zdarzenia są w outbox, potrzebujesz workera, który je odczyta i wykona wywołania do zewnętrznego API. To część, która zamienia wzorzec w niezawodną integrację.

Polling jest zwykle najprostszą opcją. LISTEN/NOTIFY może zmniejszyć latencję, ale wprowadza dodatkowe elementy i nadal potrzebuje fallbacku, gdy powiadomienia zostaną utracone lub worker się zrestartuje. Dla większości zespołów stabilny polling z małą partią jest łatwiejszy w utrzymaniu i debugowaniu.

Bezpieczne „zajmowanie” wierszy

Worker powinien zajmować wiersze, aby dwóch workerów nie przetwarzało tego samego zdarzenia jednocześnie. W PostgreSQL powszechne podejście to wybór partii używając blokad wierszy i SKIP LOCKED, a potem oznaczenie ich jako w trakcie przetwarzania.

Praktyczny przepływ statusów to:

  • pending: gotowe do wysłania
  • processing: zajęte przez workera (użyj locked_by i locked_at)
  • sent: dostarczone pomyślnie
  • failed: zatrzymane po maksymalnej liczbie prób (lub przeniesione do ręcznej weryfikacji)

Trzymaj partie małe, żeby nie obciążać bazy. Partia 10–100 wierszy co 1–5 sekund to powszechny punkt startowy.

Gdy wywołanie się powiedzie, ustaw wiersz na sent. Gdy zawiedzie, zwiększ attempts, ustaw available_at w przyszłości (backoff), zwolnij blokadę i zwróć wiersz do pending.

Logi, które pomagają (bez wycieków sekretów)

Dobre logi czynią błędy wykonalnymi. Loguj id outbox, typ zdarzenia, nazwę docelowego serwisu, liczbę prób, czasy i status HTTP lub klasę błędu. Unikaj logowania ciał żądań, nagłówków auth i pełnych odpowiedzi. Jeśli potrzebujesz korelacji, przechowuj bezpieczne request ID lub skrót zamiast surowych payloadów.

Zasady kolejności, które działają w praktyce

Dodaj ponowne próby bez duplikatów
Skonfiguruj proces wysyłający w stylu worker, który bezpiecznie próbuje ponownie i zapisuje status dostarczenia.
Wypróbuj teraz

Wiele zespołów zaczyna od „wysyłaj zdarzenia w tej samej kolejności, w jakiej je tworzyliśmy”. Problem w tym, że „ta sama kolejność” rzadko jest globalna. Jeśli wymusisz jedną globalną kolejkę, wolny klient lub niestabilne API może zatrzymać wszystkich.

Praktyczna zasada: zachowuj kolejność per grupa, a nie dla całego systemu. Wybierz klucz grupujący odpowiadający temu, jak świat zewnętrzny myśli o Twoich danych, np. customer_id, account_id lub aggregate_id jak order_id. Następnie gwarantuj kolejność wewnątrz każdej grupy, a wiele grup obsługuj równolegle.

Równoległe workery bez łamania kolejności

Uruchamiaj wielu workerów, ale upewnij się, że dwóch workerów nie przetwarza tej samej grupy jednocześnie. Zwykłe podejście: zawsze dostarczaj najwcześniejsze nie wysłane zdarzenie dla danego aggregate_id i pozwól na równoległość między różnymi agregatami.

Uprość reguły zajmowania:

  • Dostarczaj tylko najwcześniejsze oczekujące zdarzenie na grupę.
  • Pozwól na równoległość między grupami, nie w ich obrębie.
  • Zajmij jedno zdarzenie, wyślij je, zaktualizuj status, potem przejdź dalej.

Gdy jedno zdarzenie blokuje resztę

Prędzej czy później pojawi się „trujące” zdarzenie, które będzie nieudane przez wiele godzin (błędny payload, cofnięty token, awaria providera). Jeśli surowo wymuszasz kolejność w grupie, późniejsze zdarzenia tej grupy powinny czekać, ale inne grupy powinny iść dalej.

Praktyczny kompromis: ogranicz liczbę retryów dla zdarzenia. Po jej przekroczeniu oznacz je jako failed i wstrzymaj tylko tę grupę, aż ktoś naprawi przyczynę. To pozwala, by jeden zepsuty klient nie spowalniał wszystkich.

Ponowne próby bez pogarszania sytuacji

Retrye to punkt, w którym dobre ustawienie outbox staje się albo niezawodne, albo hałaśliwe. Cel jest prosty: próbuj ponownie, gdy jest szansa, że to zadziała, i szybko przestań, gdy nie ma sensu.

Stosuj eksponencjalny backoff i twardy limit. Na przykład: 1 minuta, 2 minuty, 4 minuty, 8 minut, potem przerwij (albo kontynuuj z maksymalnym opóźnieniem np. 15 minut). Zawsze ustaw maksymalną liczbę prób, żeby jedno złe zdarzenie nie zablokowało systemu na zawsze.

Nie każdy błąd powinien być retryowany. Ustal jasne reguły:

  • Retry: timeouty sieci, reset połączenia, problemy DNS oraz odpowiedzi HTTP 429 lub 5xx.
  • Nie retryuj: HTTP 400 (bad request), 401/403 (problemy z autoryzacją), 404 (zły endpoint) czy błędy walidacji wykrywalne przed wysłaniem.

Przechowuj stan retry na wierszu outbox. Zwiększ attempts, ustaw available_at dla kolejnej próby i zapisz krótki, bezpieczny opis błędu (kod statusu, klasa błędu, skrócona wiadomość). Nie przechowuj pełnych payloadów ani danych wrażliwych w polach błędu.

Rate limity wymagają specjalnego traktowania. Jeśli otrzymasz HTTP 429, respektuj Retry-After gdy istnieje. Jeśli go nie ma, odetnij się mocniej, by uniknąć fal retryów.

Podstawy deduplikacji i idempotencji

Zbuduj bezpieczniejszy przepływ integracji
Twórz niezawodne integracje z outbox w PostgreSQL i zachowaj szybkość żądań użytkowników.
Wypróbuj AppMaster

Jeśli budujesz niezawodne integracje API, zakładaj, że to samo zdarzenie może być wysłane dwukrotnie. Worker może paść po wywołaniu HTTP, zanim zapisze sukces. Timeout może ukryć sukces. Retry może nakładać się na wolne pierwsze wywołanie. Wzorzec outbox zmniejsza zagubione zdarzenia, ale sam nie zapobiega duplikatom.

Najbezpieczniejsze podejście to idempotencja: powtarzane dostawy dają ten sam efekt co jedna dostawa. Przy wywołaniu zewnętrznego API dołącz klucz idempotencyjny stabilny dla tego zdarzenia i miejsca docelowego. Wiele API wspiera nagłówek; jeśli nie, umieść klucz w ciele żądania.

Prosty klucz to kombinacja destination i event ID. Dla zdarzenia o ID evt_123 używaj czegoś jak destA:evt_123.

Po swojej stronie zapobiegaj podwójnym wysyłkom, prowadząc log dostaw i wymuszając unikalne ograniczenie jak (destination, event_id). Nawet jeśli dwóch workerów się pościga, tylko jeden będzie mógł utworzyć rekord „wysyłamy to”.

Webhooki też mogą duplikować

Jeśli otrzymujesz callbacki webhooków (np. „delivery confirmed” lub „status updated”), traktuj je tak samo. Dostawcy retryują i możesz zobaczyć ten sam payload wiele razy. Przechowuj przetworzone ID webhooków albo oblicz stabilny hash z ID wiadomości dostawcy i odrzucaj powtórzenia.

Jak długo przechowywać dane

Przechowuj wiersze outbox aż zapiszesz sukces (lub zaakceptujesz ostateczną porażkę). Dzienniki dostaw przechowuj dłużej — są Twoim audytem, gdy ktoś zapyta „czy to wysłaliśmy?”.

Powszechne podejście:

  • Wiersze outbox: usuń lub zarchiwizuj po sukcesie plus krótki okres bezpieczeństwa (dni).
  • Dzienniki dostaw: przechowuj tygodnie lub miesiące, w zależności od wymogów compliance i potrzeb wsparcia.
  • Klucze idempotencyjne: przechowuj przynajmniej tak długo, jak mogą występować retrye (a jeszcze dłużej dla duplikatów webhooków).

Krok po kroku: wdrażanie wzorca outbox

Zdecyduj, co będziesz publikować. Trzymaj zdarzenia małe, skupione i łatwe do ponownego odtworzenia. Dobra zasada: jedno zdarzenie = jeden fakt biznesowy, z wystarczającymi danymi, by odbiorca mógł zadziałać.

Zbuduj fundamenty

Wybierz jasne nazwy zdarzeń (np. order.created, order.paid) i wersjonuj schemat payloadu (np. v1, v2). Wersjonowanie pozwala dodawać pola później bez łamania starszych konsumentów.

Utwórz tabelę outbox w PostgreSQL i dodaj indeksy do zapytań, które worker będzie wykonywał najczęściej, szczególnie (status, available_at, id).

Zaktualizuj przepływ zapisu, żeby zmiana biznesowa i insert do outbox występowały w tej samej transakcji. To rdzeń gwarancji.

Dodaj dostarczanie i kontrolę

Prosty plan wdrożenia:

  • Zdefiniuj typy zdarzeń i wersje payloadów, które możesz wspierać długoterminowo.
  • Stwórz tabelę outbox i indeksy.
  • Wstaw wiersz outbox wraz ze zmianą główną danych.
  • Zbuduj workera, który zajmuje wiersze, wysyła do zewnętrznego API, a potem aktualizuje status.
  • Dodaj harmonogram retryów z backoffem i stan failed po wyczerpaniu prób.

Dodaj podstawowe metryki, żeby zauważyć problemy wcześnie: lag (wiek najstarszego niewysłanego zdarzenia), rate wysyłek i wskaźnik błędów.

Prosty przykład: wysyłanie zdarzeń zamówień do usług zewnętrznych

Zaimplementuj wzorzec wizualnie
Użyj wizualnej logiki biznesowej, aby zapisać dane i dodać zdarzenia do kolejki w jednej transakcji.
Utwórz backend

Klient składa zamówienie w Twojej aplikacji. Dwa zadania muszą się wydarzyć poza Twoim systemem: provider płatności musi obciążyć kartę, a przewoźnik musi utworzyć przesyłkę.

Z wzorcem outbox nie wywołujesz tych API w trakcie requestu checkout. Zamiast tego zapisujesz zamówienie i wiersz outbox w tej samej transakcji PostgreSQL, dzięki czemu nigdy nie skończysz z „zamówienie zapisane, ale brak powiadomienia” (ani odwrotnie).

Typowy wiersz outbox dla zdarzenia zamówienia może zawierać aggregate_id (ID zamówienia), event_type jak order.created i JSONB payload z sumami, pozycjami i danymi dostawy.

Worker pobiera następnie oczekujące wiersze i wywołuje zewnętrzne serwisy (albo w ustalonej kolejności, albo emitując oddzielne zdarzenia jak payment.requested i shipment.requested). Jeśli jeden provider jest niedostępny, worker zapisuje próbę, planuje następną próbę przez przesunięcie available_at i idzie dalej. Zamówienie dalej istnieje, a zdarzenie będzie ponawiane później bez blokowania nowych checkoutów.

Kolejność zazwyczaj wymuszana jest „per zamówienie” lub „per klient”. Wymuszaj przetwarzanie zdarzeń o tym samym aggregate_id jedno po drugim, aby order.paid nigdy nie dotarło przed order.created.

Deduplikacja chroni przed podwójnym obciążeniem karty lub tworzeniem dwóch przesyłek. Wysyłaj klucz idempotencyjny, gdy partner to wspiera, i prowadź rekord dostawy, aby retry po timeoutcie nie uruchomił drugiej akcji.

Szybkie sprawdzenia przed uruchomieniem

Daj wsparciu jasny wgląd
Dodaj autoryzację i narzędzia administracyjne do przeglądu oczekujących, wysłanych i nieudanych zdarzeń.
Zacznij teraz

Zanim zaufasz integracji do obsługi pieniędzy, powiadomień klientów czy synchronizacji danych, przetestuj skraje: crashy, retrye, duplikaty i wielu workerów.

Sprawdzające kroki, które łapią typowe błędy:

  • Potwierdź, że wiersz outbox jest tworzony w tej samej transakcji co zmiana biznesowa.
  • Zweryfikuj, że nadawca (sender) jest bezpieczny do uruchamiania w wielu instancjach. Dwóch workerów nie powinno wysłać tego samego zdarzenia jednocześnie.
  • Jeśli kolejność ma znaczenie, zdefiniuj regułę w jednym zdaniu i egzekwuj ją stabilnym kluczem.
  • Dla każdego miejsca docelowego zdecyduj, jak zapobiegać duplikatom i jak udowodnić „wysłaliśmy to”.
  • Zdefiniuj exit: po N próbach przenieś zdarzenie do failed, zachowaj ostatni skrócony opis błędu i zapewnij prostą akcję ponownego przetworzenia.

Rzeczywistość: Stripe może zaakceptować żądanie, a Twój worker padnie zanim zapisze sukces. Bez idempotencji retry może spowodować podwójną akcję. Z idempotencją i zapisanym rekordem dostawy retry staje się bezpieczny.

Następne kroki: wdrażanie bez zakłóceń aplikacji

Rollout to moment, w którym projekty outbox zwykle albo odniosą sukces, albo utkną. Zacznij mało, żeby zobaczyć rzeczywiste zachowanie bez narażania całej warstwy integracji.

Zacznij od jednej integracji i jednego typu zdarzenia. Na przykład wyślij tylko order.created do jednego API dostawcy, podczas gdy reszta działa jak wcześniej. To da Ci czystą linię bazową dla przepustowości, opóźnień i wskaźników błędów.

Uczyń problemy widocznymi wcześnie. Dodaj dashboardy i alerty dla opóźnień outbox (ile zdarzeń czeka i jak stare jest najstarsze) oraz wskaźnika błędów (ile jest utkniętych w retry). Jeśli możesz odpowiedzieć „czy jesteśmy opóźnieni teraz?” w 10 sekund, złapiesz problemy zanim zauważą to użytkownicy.

Miej plan bezpiecznego ponownego przetwarzania przed pierwszym incydentem. Ustal, co oznacza „reprocess”: ponów ten sam payload, odbuduj payload z aktualnych danych czy wyślij do ręcznej weryfikacji. Udokumentuj, które przypadki są bezpieczne do ponownego wysłania, a które wymagają kontroli człowieka.

Jeśli budujesz to na platformie no-code takiej jak AppMaster, ta sama struktura nadal obowiązuje: zapisz dane biznesowe i wiersz outbox razem w PostgreSQL, a potem uruchom oddzielny proces backendowy do dostarczania, retryów oraz oznaczania zdarzeń jako wysłane lub nieudane.

FAQ

Kiedy powinienem użyć wzorca outbox zamiast wywoływać API bezpośrednio?

Użyj wzorca outbox, gdy akcja użytkownika aktualizuje Twoją bazę danych i musi wywołać pracę w innym systemie. Jest szczególnie przydatny, gdy timeouty, niestabilne sieci lub awarie partnerów mogą spowodować sytuacje „zapisane w naszej aplikacji, brak w ich systemie”.

Dlaczego zapis do outbox musi być w tej samej transakcji co zapis biznesowy?

Zapisanie wiersza biznesowego i wiersza outbox w tej samej transakcji daje jedną prostą gwarancję: albo oba istnieją, albo żaden nie istnieje. Dzięki temu unikasz częściowych porażek, np. „wywołanie API powiodło się, ale zamówienie nie zostało zapisane” albo „zamówienie zapisane, ale wywołanie API nie doszło do skutku”.

Jakie pola powinna zawierać tabela outbox, by być praktyczną?

Dobry zestaw pól to prosty kompromis między wygodą a kontrolą. Standardowo warto mieć id, aggregate_id, event_type, payload, status, created_at, available_at, attempts oraz pola blokujące jak locked_at i locked_by. To upraszcza wysyłanie, planowanie ponownych prób i bezpieczną współbieżność bez nadmiernego komplikowania tabeli.

Jakie indeksy są najważniejsze dla tabeli outbox w PostgreSQL?

Podstawowy indeks to (status, available_at, id), dzięki któremu worker szybko znajdzie kolejne wysyłalne zdarzenia w kolejności. Dodawaj inne indeksy tylko wtedy, gdy rzeczywiście po nich zapytujesz — nadmiar indeksów spowalnia wstawienia.

Czy mój worker powinien pollować tabelę outbox czy użyć LISTEN/NOTIFY?

Polling jest najprostszą i najbardziej przewidywalną opcją dla większości zespołów. Zacznij od małych partii i krótkiego interwału, a potem dostosuj według obciążenia i opóźnień. LISTEN/NOTIFY może zmniejszyć latencję, ale dodaje złożoność i i tak potrzebuje zapasowego mechanizmu, gdy powiadomienia zostaną utracone.

Jak zapobiec sytuacji, że dwóch workerów wyśle to samo zdarzenie z outbox?

Zabezpiecz wiersze przy użyciu blokad na poziomie wiersza, aby dwóch workerów nie przetwarzało tego samego zdarzenia równocześnie — zwykle z SKIP LOCKED. Następnie oznacz wiersz jako processing z timestampem i identyfikatorem worker’a, wyślij i na końcu ustaw sent lub przywróć do pending z przyszłym available_at.

Jaka jest najbezpieczniejsza strategia ponownych prób dla dostarczania z outbox?

Stosuj eksponencjalny backoff z twardym limitem prób i retryuj tylko te błędy, które prawdopodobnie są tymczasowe. Dobre kandydaty do retry to timeouty, błędy sieci, reset połączenia oraz odpowiedzi HTTP 429 i 5xx. Błędy walidacji i większość odpowiedzi 4xx traktuj jako ostateczne, dopóki nie poprawisz danych lub konfiguracji.

Czy wzorzec outbox gwarantuje dostarczenie dokładnie raz?

Nie zakładaj dostarczenia dokładnie raz. Duplikaty mogą się zdarzyć, np. gdy worker padnie po udanym wywołaniu HTTP, zanim zapisze sukces. Używaj klucza idempotencyjnego stabilnego dla danego zdarzenia i miejsca docelowego oraz prowadź dziennik dostaw z unikalnym ograniczeniem jak (destination, event_id), żeby zapobiegać podwójnym akcjom przy wyścigach.

Jak obsługiwać kolejność bez spowalniania całego systemu?

Zachowuj kolejność w grupach, a nie globalnie. Wybierz klucz grupujący (np. aggregate_id, customer_id), zagwarantuj kolejność w obrębie tej grupy, a równolegle przetwarzaj różne grupy. Dzięki temu jeden wolny klient nie blokuje wszystkich.

Co zrobić z „trującym” zdarzeniem, które ciągle się nie udaje?

Po osiągnięciu maksymalnej liczby prób oznacz zdarzenie jako failed, zachowaj krótki, bezpieczny opis błędu i wstrzymaj przetwarzanie dalszych zdarzeń dla tej samej grupy, dopóki ktoś nie naprawi przyczyny. To ogranicza zasięg problemu i zapobiega niekończącym się ponownym próbom.

Łatwy do uruchomienia
Stworzyć coś niesamowitego

Eksperymentuj z AppMaster z darmowym planem.
Kiedy będziesz gotowy, możesz wybrać odpowiednią subskrypcję.

Rozpocznij