Start po roku: lawina użytkowników, dym z bazy i presja na „przepiszmy wszystko”
Dziesiąty miesiąc od startu. Akcja sponsorowana na dużym portalu dociąga tysiące nowych rejestracji na minutę. Panel admina zaczyna ładować się kilkadziesiąt sekund, płatności przetwarzają się podwójnie, a dashboardy w monitoringach gasną jeden po drugim. Zespół dokłada kolejne instancje, ale to nic nie zmienia — baza danych jest szyjką butelki, kolejki zapełniają się szybciej, niż konsumerzy są je w stanie opróżniać. Na Slacku pada hasło: „przepiszmy na mikroserwisy i będzie po kłopocie”. Tyle że do kampanii numer dwa zostały trzy tygodnie.
Nie każda firma musi obsługiwać ruch jak globalna platforma, ale każdy produkt może trafić na ścianę, jeśli architektura skalowalna nie była projektowana od dnia zero. Chodzi o kilka decyzji, które są tanie na początku, a kosztowne w momencie kryzysu: jak ciąć domenę, które ścieżki robić synchronicznie, jak modelować dane i kontrakty, jakie metryki uznać za nieprzekraczalne. Zamiast recept uniwersalnych — scenariusze, kryteria i kompromisy, które pomagają nie przepisać produktu po roku.
Krótkie pytania, które zwykle padają przed „przepiszmy to”
- Czy i kiedy wybrać modularny monolit zamiast mikroserwisów?
- Które operacje muszą być synchroniczne, a które można bezpiecznie przenieść do kolejek?
- Jak modelować dane, żeby uniknąć blokad i pełnych skanów tabel pod obciążeniem?
- Jak wprowadzać zmiany w API i schematach bez zrywania produkcji?
- Jakie metryki i alerty realnie ochronią przed awarią, a nie tylko „świecą na czerwono”?
- Jak wdrażać i migrować dane bez przestoju przy rosnącej skali?
- Na czym uruchomić produkt na start, by nie wpaść w pułapkę kosztów lub lock-inu?
W kolejnych sekcjach znajdziesz konkretne odpowiedzi i kryteria podejmowania decyzji, ilustrowane sytuacjami, w których coś poszło źle i jak tego uniknąć.
Granice domeny i modularny monolit zamiast rozproszonego chaosu
Cięcie domeny: jak zdefiniować granice, które przetrwają wzrost
Skalowalność zaczyna się od granic. Jeśli cała logika biznesowa miesza się w jednym katalogu, każdy feature wpływa na wszystko. Lepszym podejściem jest podział na wyraźne moduły domenowe (np. Zamówienia, Płatności, Katalog, Użytkownicy), z jasno określonymi modelami i interfejsami. Pomaga tu myślenie w kategoriach bounded contexts: dany termin ma znaczenie w danym kontekście i nie „przecieka” poza niego. Przykład: „status” zamówienia to nie „status” dostawy; jeśli trzymasz je w jednej kolumnie, zapraszasz problemy.
Reguła praktyczna: jeśli moduł może być zrozumiany, zbudowany i przetestowany w izolacji przez mały zespół, to jest to dobre cięcie. Jeśli musisz wczytywać się w globalne „helpersy” i typy, by wprowadzić zmianę, granice są sztuczne.
Scenariusz z życia: funkcja naliczania rabatów rozsiana po wielu miejscach, bo „było szybciej”. Po zwiększeniu liczby promocji zmiany powodują regresje poza modułem Płatności. Gdy każda część systemu aktualizuje stan zamówienia na swój sposób, rośnie sprzężenie i liczba nieoczekiwanych interakcji. Rozwiązanie: wyizolować polityki rabatowe w dedykowanym module, ujawniać jeden kontrakt naliczania i rozliczania zleceń.
Modularny monolit: separacja bez rozproszenia
„Mikroserwisy od dnia zero” często kończą się rozproszoną transakcją i większym ryzykiem niż zyskiem. Modularny monolit daje kontrolę transakcji w obrębie jednej bazy i procesu, a jednocześnie wymusza dyscyplinę granic: osobne pakiety, publiczne interfejsy, zakaz bocznego dostępu do cudzych tabel, wymuszony przepływ zdarzeń domenowych i jawne zależności.
Techniczne zasady pomocne od startu: ukryj modele wewnętrzne modułu (np. internal packages), wystawiaj tylko interfejsy usług lub zdarzenia; blokuj zależności cykliczne w CI (np. regułami repozytorium); testuj integrację między modułami kontraktami, nie danymi wewnętrznymi.
Ścieżki synchroniczne kontra kolejki: gdzie ryzyko, gdzie zysk
Budżet latencji: co naprawdę musi być „tu i teraz”
Nie każda operacja użytkownika wymaga natychmiastowego efektu. Reguła praktyczna: odpowiedź interfejsu musi potwierdzać tylko to, co użytkownik postrzega jako finalny stan krytyczny (np. złożenie zamówienia, rezerwacja miejsca), a resztę można opóźnić. Jeśli budżet p99 dla akcji w UI to 300–500 ms, każdy dodatkowy skok sieciowy i blokujący call do zewnętrznego dostawcy usuwa margines błędu. Zamiast synchronizować wszystko, zamknij krytyczną część w transakcji lokalnej, a walidacje nieblokujące i notyfikacje przenieś do zdarzeń/wykonywania asynchronicznego.
Przykład: podczas checkoutu synchronizowanie skoringu ryzyka z usługą zewnętrzną obniża p99 i mnoży błędy. Lepszy kompromis: lokalna decyzja w oparciu o cache/reguły, a pełna weryfikacja i ewentualne wstrzymanie wysyłki — asynchronicznie, z możliwością cofnięcia zamówienia przed realizacją.
Idempotencja i „dokładnie raz”: mit i praktyka
Kolejki i zdarzenia zwykle dostarczają co najmniej raz. Jeśli handler nie jest idempotentny, zaczną się podwójne obciążenia kart i zduplikowane wysyłki. Minimum: generuj stabilne identyfikatory operacji (requestId/orderId), trzymaj dziennik zrealizowanych operacji z unikalnym kluczem, stosuj „transakcyjny outbox” przy publikacji zdarzeń (zapisy i publikacja w jednej transakcji). „Exactly once” pojawia się w marketingu — w praktyce projektuj „co najmniej raz” + deduplikację po stronie konsumenta.
Kiedy kolejka szkodzi zamiast pomagać
Kolejka nie rozwiązuje problemów z integralnością. Nie przenoś do niej operacji wymagających silnych niezmienników (np. rozliczenia salda w wielu walutach) bez pojedynczego piszącego lub serializacji po kluczu. Nie używaj jej też jako RPC — request/response przez MQ zwykle zwiększa latencję i utrudnia backpressure. I jeszcze jedno: długa kolejka ukrywa przeciążenie. Ustal twardy limit lagów i czasy życia wiadomości; jeśli system nie nadąża, odrzuć lub degraduj funkcję, zamiast akumulować godziny opóźnień.
Model danych odporny na wzrost obciążenia
Kształt zapytań i indeksów zgodny z ruchem
Pełne skany tabel i losowe dostępy po kolumnach bez indeksów zemszczą się przy wzroście. Indeksy złożone dopasuj do WHERE i kolejności sortowania, zamiast „indyksować wszystko po kolei”. Unikaj OFFSET/LIMIT przy dużych zbiorach — przejdź na keyset pagination. Policz, które odczyty dominują i czy można je zamienić na zmaterializowane widoki/cache. Jeśli API zwraca listy, nie serwuj SELECT *, tylko konkretny zestaw pól, by nie pompuć I/O.
Gorące rekordy i partycjonowanie
Pojedynczy licznik, globalna kolejka „ostatnich zamówień” czy przyrosty po rosnącym kluczu potrafią utworzyć gorące partycje lub stronice indeksu. Rozpraszaj zapisy: sharding po kluczu biznesowym, „sharded counters”, albo identyfikatory czasowo-uporządkowane (np. UUIDv7) zamiast skoków po losowym UUIDv4, który degraduje lokalność indeksów. Jeśli ruch skupia się na jednym tenantcie, rozważ osobną partycję/instancję na poziomie organizacji, zamiast „wypychać” wszystko przez jeden węzeł.
Transakcje krótkie i świadomy poziom izolacji
Im dłużej trzymasz transakcję, tym bardziej rośnie ryzyko blokad. Zmień nawyk: odczyty ciężkie poza transakcją modyfikującą, a jeśli potrzebujesz blokady — użyj SELECT … FOR UPDATE SKIP LOCKED i kolejkuj pracę partiami. Nie podnoś izolacji do SERIALIZABLE „na wszelki wypadek”; często wystarczy READ COMMITTED + niezmienniki po stronie aplikacji. A migracje, które skanują całe tabele, uruchamiaj z ograniczaniem tempa i poza godzinami szczytu (w Postgresie: CREATE INDEX CONCURRENTLY, rozsądne batch size i przerwy).
Event sourcing czy zwykły CRUD
Event sourcing bywa pomocny, gdy stan jest pochodną serii zdarzeń, a audyt/replay to realny wymóg. Koszt to złożoność projekcji, migracji zdarzeń i spójność czytelni. Jeśli audyt to tylko „kto i kiedy zmienił”, zwykły CRUD z dziennikiem zmian i outboxem jest prostszy i tańszy. Wyjątek: domeny o naturze finansowej lub procesowej, gdzie spory i odtwarzanie stanu są chlebem powszednim.
Ewolucja kontraktów i schematów bez zrywania produkcji
Zmiany kompatybilne w przód i wstecz
Dodawaj, nie usuwaj. Najpierw wprowadź nowe pole/endpoint jako opcjonalny, opublikuj kontrakt i poczekaj, aż konsumenci zaczną go czytać/zapisywać. Dopiero po okresie przejściowym usuwaj stare elementy. Twarde złamania wersji odkładaj na rzadkie okna migracyjne i tylko z planem cofnięcia.
Wersjonowanie API i schematów bez traumatycznych cięć
Kontrakty ewoluują najbezpieczniej, gdy konsumenci są „tolerant readers”, a producenci „liberal writers”. W REST wersja w ścieżce bywa praktyczna operacyjnie (łatwy routing i logi), ale mnoży endpointy. Wersjonowanie przez nagłówki jest czystsze semantycznie, lecz gorzej widoczne i częściej pomijane. Niezależnie od wyboru: dodawaj pola z sensownymi domyślnymi wartościami i nie zmieniaj znaczenia istniejących. Zamiana typu (np. liczby na string) to realne złamanie; bez długiej fazy dwuwariantowej klienci upadną w najmniej wygodnym momencie. W GraphQL oznaczanie pól jako @deprecated nie usuwa długu, tylko przesuwa go w czasie — plan usunięcia i telemetryka użycia są konieczne.
Po stronie bazy zadziała ta sama zasada: najpierw rozszerz schemat (kolumna opcjonalna, nowa tabela), naucz aplikację pisać i czytać oba formaty, dopiero potem kontrakcja. Złe skróty: migracja, która zmienia kolumnę NOT NULL i blokuje zapis na czas skanu; refaktoryzacja kluczy, która łamie cudze raporty, bo „nikt tego nie używał”. Telemetria odczytów i audit log redukują zgadywanie.
Expand–contract w krótkich krokach
- Dodaj nową strukturę obok starej (kolumna/tabela/pole w API), oznacz ją jako opcjonalną.
- Włącz podwójny zapis (stary i nowy format) oraz odczyt z preferencją nowego, ale z fallbackiem.
- Uruchom backfill w tle z limitem tempa i obserwuj p95/p99 zapytań oraz blokady.
- Przełącz czytanie wyłącznie na nowy format po uzyskaniu pełnego pokrycia danych.
- Usuń stary format w osobnym wdrożeniu, po okresie obserwacji i z planem cofnięcia.
Wdrażanie bez przestoju i szybkie cofanie
Flagi funkcji jako bezpiecznik, nie stała konfiguracja
Flagi pozwalają ograniczyć zasięg ryzyka i wyłączyć funkcję bez rollbacku binariów. Pułapka: wieczne flagi i spaghetti warunków. Reguła: każda flaga ma właściciela, termin ważności i kryterium sukcesu; po migracji znika. Krytyczne ścieżki miej pod parasolem „kill switch” (np. wysyłka powiadomień, kosztowne integracje). I jeszcze jedno: decyzja flagi musi być tania i deterministyczna — zbyt częste odpytywanie zewnętrznej konfiguracji lub złożone reguły wstrzykną losowość w metryki.
Stopniowe rollouty i mały promień rażenia
Canary ma sens, jeśli mierzysz to, co istotne: błąd na ścieżce krytycznej, p95 latencji, odsetek time-outów do kluczowych dostawców. „CPU spadł, więc jest dobrze” to fałszywe poczucie bezpieczeństwa. Blue/green ułatwia szybki powrót, ale bywa drogi przy stanowych komponentach (cache rozgrzewane osobno, dwie floty). Przy zmianach schematu unikaj kanarków tylko po stronie aplikacji — kanarek, który pisze nowy format, a reszta klastrów jeszcze nie czyta, to przepis na rozjazd danych. Pomaga shadow traffic: równoległe zapisy/odczyty bez wpływu na użytkownika i porównywanie wyników.
Migracje danych w ruchu
Backfill w partiach z priorytetem tła redukuje cios w produkcję. W relacyjnych bazach: indeksy twórz współbieżnie, a długie aktualizacje tnij na małe zakresy po kluczu i rób przerwy. Kolejny detal, który ratuje: limit czasu zapytania w jobach migracyjnych, by nie trzymać blokad „w nieskończoność”. Przykład z życia: migracja, która aktualizowała miliony rekordów jedną transakcją, zakończyła się kaskadą deadlocków i rollbackiem po 40 minutach; ta sama migracja w porcjach po 1000 rekordów przeszła bez alarmów.
Obserwowalność, która prowadzi decyzje
Sygnały użytkownika ponad metrykami hosta
Cztery sygnały na start: błędy, latencja (p95/p99), przepustowość i nasycenie. Dla ścieżek krytycznych zdefiniuj SLI i SLO z budżetem błędów. Gdy budżet się kurczy, ogranicz rollouty, nawet jeśli CPU wygląda zdrowo. Metryki infrastruktury są pomocnicze — użytkownik nie widzi „90% CPU”, widzi „nie mogę zapłacić”.
Alerty oparte na symptomach i kilku oknach czasowych
Progi oparte na SLO (np. 1% błędów p95 w 10 min i 0,1% w 1 h) zmniejszają trzepotanie alertów. Alerty z wnętrzności (GC, load average) trzymaj jako obserwacje, nie wyzwalacze nocnych telefonów. Zadbaj o niską krotność kardynalną w metrykach — etykiety z userId w logach licznikowych zabiją system monitoringu i podniosą rachunek.
Kontrola przeciążenia i łagodna degradacja
Limity i kontrola przyjęć zanim będzie za późno
Bez twardych limitów równoległości każdy wzrost ruchu kończy się kaskadą timeoutów. Wprowadź limity per tenant, per klucz biznesowy i per zależność zewnętrzną. Przyjmuj pracę tylko, jeśli masz budżet zasobów („admission control”), inaczej odrzuć wcześnie (HTTP 429/503) z przewidywalnym komunikatem. Token bucket lub leaky bucket na brzegu systemu są prostsze w utrzymaniu niż rozproszone semafory w środku grafu usług.
Circuit breaker odcina źle działające zależności, ale nie zastąpi budżetu zapytań. Pułapka: breaker z progiem na błędy 5xx, a realną przyczyną są powolne 200 OK. Mierz opóźnienia i odsetek timeoutów, nie tylko statusy.
Budżety czasowe zamiast „jeden timeout na wszystko”
Wyznacz end-to-end deadline dla żądania (np. 1,5 s) i rozbij go na wewnętrzne budżety: cache 50 ms, DB 200 ms, dostawca płatności 600 ms, renderowanie 200 ms. Każdy komponent ma własny timeout i deadline propagation w kontekście. Reguła: timeout p50, aby uniknąć fałszywych przerwań. Jeśli zależność regularnie zbliża się do budżetu — degraduj ścieżkę, zamiast „na chwilę” podnosić timeout. Ten wyjątek działa tylko w migracjach i musi mieć datę wygaśnięcia.
Degradacja, która ratuje biznes, nie tylko serwery
Kiedy zaczyna brakować zasobów, wyłączaj najdroższe i najmniej krytyczne elementy: podglądy wideo, sortowania „po trafności” wymagające wektorowego wyszukiwania, synchroniczne webhooki. Zastępuj dynamiczne dane cachem stale-while-revalidate i przywracaj świeżość w tle. Zmniejszaj rozdzielczość wyników (mniej elementów na stronie, uboższe pola), zamiast odrzucać wszystko. W integracjach przechodź na tryb „fire-and-forget” z outboxem i powtórkami, jeśli akceptowalna jest spóźniona dostawa.
Testy obciążeniowe, które mówią prawdę
Model ruchu: otwarty kontra zamknięty
Testy „10 wątków przez 5 minut” rzadko odzwierciedlają produkcję. W systemach internetowych lepszy jest model otwarty z ustaloną arrival rate (np. 200 RPS), bo oddaje wzrost napływu niezależnie od opóźnień. Model zamknięty (N stałych użytkowników) maskuje przeciążenie — żądania czekają, a RPS nie rośnie, więc wnioski są zafałszowane.
Mały, realistyczny plan przed premierą
Trzy kroki wystarczą, aby złapać 80% problemów: rozgrzewka (cache, JIT, połączenia), stopniowy step load do momentu „kolana” krzywej latencji, a potem test nasyceniowy z wymuszeniem backpressure. Dane testowe muszą odzwierciedlać rozkłady (kilku „dużych” tenantów, wiele małych), inaczej testy przepuszczą hotspoty. Jeśli Twoja architektura zakłada keyset pagination — uwzględnij długie ogony list, bo pierwsze strony rzadko są problemem.
Koszt jednostkowy i margines bezpieczeństwa
Policz koszt zapytania: CPU-ms, I/O i zewnętrzne wywołania na jedną transakcję. Zestaw to z kosztem infrastruktury i ustal docelowy koszt jednostkowy. Wzrost o rząd wielkości ruchu bywa do udźwignięcia, jeśli masz margines w kosztach; bez tego każdy skok kończy się kontrowersyjną optymalizacją „na produkcji”. Dla refactorów wydajnościowych wymagaj regresji kosztu jednostkowego w testach porównawczych jako kryterium akceptacji.
Wieloregionowość i odtwarzanie po awarii bez heroizmu
Jedno źródło zapisu, wiele czytelni — domyślna ścieżka
Multi-master brzmi kusząco, lecz rozwiązuje nie ten problem co trzeba. Domyślnie trzymaj jeden region jako zapis, replikuj do czytelni, a klientów kieruj regionalnie po odczyt. Pinning per tenant upraszcza spójność. Wyjątki (czat, wspólne edytowanie) mogą potrzebować CRDT lub konfliktów na brzegu — ale to projekty z osobnym rachunkiem złożoności.
Failover planowany i ćwiczony
RTO/RPO to nie slajdy. Przećwicz przełączenie: zamrożenie zapisów, promocja repliki, rekonfiguracja kolejek i TTL DNS w realnych wartościach, nie „60 s w teorii”. Shadow writes do zapasowego regionu pozwalają wykryć rozjazdy schematu przed awarią. Najczęstsza pułapka: nowy region nie ma rozgrzanego cache i limitów — po failoverze przechodzi z 0 do N i dusi bazę.
- Trenuj kwartalny „game day” z odcięciem krytycznej zależności i sprawdzeniem RTO/RPO.
- Utrzymuj te same limity, flagi i wersje schematów w obu regionach.
- Po powrocie unikaj „split brain” — jasno zdefiniuj kierunek resynchronizacji i politykę konfliktów.
Platforma minimalna, która skaluje się z zespołem
Automaty, które spłacają się od pierwszego dnia
Infrastruktura jako kod, pipeline z artefaktem niezmiennym, szablony serwisu (telemetria, health-checki, limity domyślne) i tajemnice w managerze kluczy — to kroki o wysokim zwrocie. Konteneryzacja bez rejestru obrazów, skanowania i budowy hermetycznej jest półśrodkiem. Runbook w repo (nie na wiki) i jeden przycisk do skalowania/wyłączania zależności ograniczają chaos w nocy.
Szybkość kontra przenośność dostawcy
Umiarkowana zależność od usług zarządzanych jest sensowna na starcie: mniej operacji, szybsze iteracje. „Warstwa abstrakcji na wszelki wypadek” często droższa niż hipotetyczna migracja. Zamiast pisać własny broker nad chmurami, trzymaj cienką warstwę adaptacyjną w miejscach styku (storage, kolejki, IAM) i kontrakty w testach integracyjnych. Gdy koszt jednostkowy lub ryzyko blokady rosną — dopiero wtedy planuj wyjście kontrolowane, sekcja po sekcji.
Odpowiedzialność operacyjna wbudowana w architekturę
Runbook, SLO i właściciel na każdy krytyczny przepływ
Każda ścieżka krytyczna (płatność, rejestracja, dostawa zdarzeń) ma mieć SLO, runbook i wskazanego właściciela on-call. Bez właściciela flagi nie znikają, a „tymczasowe” obejścia stają się stałe. Reguła zespołowa: zmiana, która pogarsza p95 lub zwiększa koszty, musi pokazać plan spłaty długu i mechanizm odwrócenia.

Telemetria domenowa jako część kontraktu
Sam 200 OK nie mówi, czy biznes żyje. Emituj metryki domenowe: odsetek udanych płatności, czas weryfikacji KYC, liczbę spóźnionych webhooków. Konsumenci usług opierają swoje alerty na tych sygnałach, nie na CPU i RAM dostawcy. Bez takiego kontraktu integracje rozpadają się przy pierwszym wzroście ruchu, bo każdy patrzy w inne wskaźniki.
Skalowalność od dnia zero to suma małych zabezpieczeń: twarde limity, jasne budżety czasowe, realistyczne testy i ćwiczone scenariusze awarii — małe, odwracalne kroki wygrywają z wielkimi przebudowami po fakcie.
Zmiany bez przestojów: kompatybilność kontraktów
Migracje schematu, które nie bolą użytkownika
Bezpieczna ścieżka to zasada „rozszerz, zmigruj, skurcz” (expand–migrate–contract). Najpierw dodajesz nowe kolumny/tabele z wartościami domyślnymi i bez twardych ograniczeń, potem migrujesz dane porcjami, a dopiero na końcu usuwasz stare pola i włączasz NOT NULL/UNIQUE. Renamy zamień na „dodaj nowe + wypełnij + przełącz odczyty + usuń stare”, bo bezpieczne „RENAME COLUMN” w godzinach szczytu to mit w wielu silnikach.
Indeksy buduj online i w oknach o niskim ruchu. Długie transakcje blokują katalogi i windują opóźnienia; lepiej 1000 małych porcji z kontrolą backpressure niż jedna heroiczna migracja. NOT NULL włączaj dopiero po weryfikacji, że nie ma „dziur” z okresu backfillu. Unikalność egzekwuj przez najpierw czyszczenie duplikatów, potem indeks częściowy, a na końcu pełny constraint, inaczej dodasz presję na hot-shardy i wywołasz lawinę konfliktów.
Dual-writes wymagają idempotencji i obserwowalności. Zanim przełączysz odczyty na nowe pole, zbierz metrykę rozjazdu (np. porównanie wartości starych i nowych) na losowej próbie. Trigger w bazie kusi, ale bywa trudny w wersjonowaniu i cofaniu; zazwyczaj prostszy jest kod aplikacyjny z flagą i buforem retry.
Kontrakty API i zdarzeń z myślą o ewolucji
Reguła domyślna: zmiany dodające są bezpieczne, zmiany usuwające lub przekształcające są ryzykowne. Nie przeciążaj istniejących pól nową semantyką. Dla JSON trzymaj tolerancję na nieznane pola po stronie konsumenta i nie polegaj na kolejności. Dla Avro/Protobuf trzymaj zgodność ewolucyjną i testy up/down-castu. Usuwanie pola to proces: najpierw oznacz jako przestarzałe w kontrakcie, zbierz dowód braku użycia w logach i dopiero po czasie usuń w schemacie.
Zdarzenia są niezmienne. Korekty wysyłaj jako nowe typy (np. OrderCorrected), nie nadpisuj starego rekordu. Schematy wersjonuj w rejestrze, a konsumentów odpalaj w trybie „najpierw przetwarzanie, potem deploy nowych producentów”. Przypadek z życia: producent dodał wymagane pole bez defaultu, stary konsument odrzucał zdarzenia i kolejka spuchła w godzinę. Ten scenariusz wychodzi w testach kontraktowych między repozytoriami, nie w testach jednostkowych.
Idempotencja i powtórki zamiast iluzji „exactly once”
Klucze idempotencji i semantyka powtórzeń
Sieć gubi odpowiedzi, klienci ponawiają. Jeśli endpoint modyfikuje stan, oczekuj powtórek. Klucz idempotencji (np. nagłówek lub pole w ciele) powiąż z efektem operacji i trzymaj w store o ograniczonym TTL. Powtórzone żądanie zwraca ten sam wynik lub 200 z referencją do oryginału; 409 na duplikat zwykle szkodzi klientom retry. Granice: nie mieszaj idempotencji z deduplikacją biznesową — klucz powinien odzwierciedlać jedną intencję (np. „charge_123”), a nie „dowolną płatność dziś”.
Dla unikalnych działań (płatności, tworzenie zasobu) wymuś unikalność przez constraint na (tenant_id, idempotency_key). Jeśli baza zwróci konflikt, mapujesz go na bezpieczną odpowiedź. Cichy wyjątek: operacje częściowo idempotentne, jak dołączanie elementu do zestawu — tu klucz musi obejmować zarówno zestaw, jak i element, inaczej utracisz dokładność.
Outbox/inbox i strumienie z CDC
Dla integracji asynchronicznych użyj transactional outbox: w jednej transakcji zapisujesz zmianę i wpis do tabeli outbox, a niezależny proces publikuje go do kolejki. Dzięki temu nie masz „ducha” zdarzenia bez źródłowego zapisu ani zapisu bez zdarzenia. CDC (np. Debezium) upraszcza publikację, ale przenosi odpowiedzialność za porządek i filtrację — utrzymuj klucze partycjonowania zgodne z agregatem, żeby zachować kolejność w ramach encji.
Po stronie konsumenta zbuduj inbox: rejestr przetworzonych wiadomości z deduplikacją po message_id i stanem efektu. Retry są domyślne, więc logika musi być odporna na duplikaty i reordering. Jeśli porządek ma znaczenie, trzymaj bufor per klucz i krótkie okno oczekiwania; pełna globalna kolejność jest droga i rzadko konieczna.





























