Synchronizacja stanów magazynowych między wieloma lokalizacjami to jeden z tych problemów, które wyglądają prosto na diagramie, ale rozpadają się przy pierwszym kontakcie z produkcją. Mieliśmy system z centralnym magazynem, halą produkcyjną i dziesięcioma restauracjami – każda lokalizacja wykonywała operacje wpływające na globalny stan. Teoretycznie prosta sprawa: ktoś coś zabiera, licznik się zmniejsza, inni to widzą. W praktyce okazało się, że „widzą” to kluczowe słowo, które kryje pół tuzina problemów architektonicznych.
Pierwszy tydzień po wdrożeniu zaczęliśmy dostawać zgłoszenia: restauracja składa zamówienie na produkt, który system pokazuje jako dostępny, dostaje potwierdzenie, a kilka minut później informację, że jednak towaru nie ma. Albo inna wersja – dwie restauracje zamawiają ten sam produkt w odstępie 30 sekund, obie widzą dostępność 50 sztuk, obie zamawiają po 40. System przyjmuje oba zamówienia, bo w momencie składania każde wyglądało sensownie. Dopiero przy próbie alokacji na magazynie okazuje się, że przydzieliliśmy 80 sztuk z 50 dostępnych.
Problem nie leżał w bazie danych ani w transakcjach. Leżał w fundamentalnym założeniu, że jesteśmy w stanie utrzymać „jeden aktualny stan” widoczny jednocześnie przez wszystkie lokalizacje. W systemie rozproszonym taki stan nie istnieje. Istnieje tylko szereg stanów lokalnych z różnym stopniem nieaktualności.
Fizyczne ograniczenia rozproszonego systemu
Zanim zaczęliśmy rozwiązywać problem, musieliśmy zrozumieć, czego w ogóle możemy realistycznie oczekiwać. CAP theorem mówi, że w rozproszonym systemie możesz mieć maksymalnie dwa z trzech: consistency, availability, partition tolerance. W naszym przypadku partition tolerance nie była opcjonalna – musiała istnieć, bo restauracje muszą działać nawet gdy połączenie z centralą jest kiepskie. Availability też była krytyczna – restauracja nie może przestać działać, bo centralny magazyn akurat coś przetwarza. Zostało nam consistency – i to ona musiała ustąpić.
Pierwotnie próbowaliśmy synchronicznego podejścia. Każde zamówienie z restauracji robiło od razu lock na rekordzie w centralnej bazie, sprawdzało dostępność, rezerwowało, commitowało. Brzmi rozsądnie. Problem pojawił się przy skalowaniu – gdy trzy restauracje jednocześnie próbowały zamówić różne produkty z tego samego obszaru magazynu, zaczynały na siebie czekać. Średni czas składania zamówienia skoczył z 2 do 15 sekund w godzinach szczytu. Deadlocki stały się codziennością.
Próbowaliśmy optymistycznych locków – każdy rekord miał wersję, przy zapisie sprawdzaliśmy czy wersja się zgadza. Zmniejszyło to liczbę deadlocków, ale stworzyło nowy problem: teraz restauracje często dostawały błąd „dane się zmieniły, spróbuj ponownie”, co wymagało ręcznego ponawiania zamówienia. UX był okropny.
Przełomem było zdefiniowanie, co tak naprawdę oznacza „real-time” w naszym kontekście. Nie oznacza to, że każda restauracja widzi dokładnie ten sam stan w dokładnie tym samym momencie. Oznacza to, że widzi stan wystarczająco aktualny, by podejmować sensowne decyzje. Ustaliliśmy SLA: stan magazynu widoczny w restauracji nie może być starszy niż 5 sekund w normalnych warunkach. To dało nam przestrzeń na eventual consistency.
Event-driven architecture jako fundament
Przestawiliśmy się na architekturę opartą o eventy. Każda operacja wpływająca na stan magazynu generowała event: StockReserved, StockIssued, StockReceived, StockAdjusted. Te eventy trafiały do brokerów – użyliśmy Redis Pub/Sub, bo dla naszej skali był wystarczający i nie wymagał operowania kolejnym ciężkim narzędziem typu Kafka.
Write path był prosty i silnie spójny. Restauracja wysyłała komendę ReserveStock do API magazynu. API sprawdzało dostępność, tworzyło rezerwację w transakcji, publikowało event. Kluczowe było to, że transakcja w bazie i publikacja eventu musiały być atomowe – albo oba się udają, albo żadne. Użyliśmy transactional outbox pattern: event najpierw zapisywany w tej samej transakcji co zmiana danych, potem osobny worker go publikował. Jeśli worker padł, event czekał w outbox i był publikowany przy następnym uruchomieniu.
Read path był eventual consistent. Każda lokalizacja miała lokalną projekcję stanów magazynowych, aktualizowaną na podstawie eventów. Projekcja to nie był pełny stan całego magazynu – to była view dostosowana do potrzeb danej lokalizacji. Restauracja widziała dostępność produktów, które może zamówić. Centrala widziała szczegółowy stan z podziałem na partie i daty ważności. Hala produkcyjna widziała dostępność surowców i półproduktów.
Te projekcje były trzymane w Redis po stronie aplikacji webowej. Przychodzący event aktualizował projekcję, która była następnie pushowana przez WebSocket do połączonych klientów. Event mógł przyjść z 100ms opóźnieniem, ale gdy już dotarł, klient dostawał update praktycznie natychmiast.
CQRS w praktyce magazynowej
Oddzielenie write i read modelu brzmi akademicko, ale ma konkretne konsekwencje dla implementacji. Write model w naszym przypadku był skoncentrowany wokół partition i lokalizacji. Rezerwacja towaru to: wybierz partie według FIFO, zablokuj je, stwórz rekord rezerwacji powiązany z konkretnymi partiami, opublikuj event. Ten kod był synchroniczny, transakcyjny, pesymistycznie lockowany. Musiał być poprawny, bo tu następowała faktyczna zmiana stanu.
Read model był denormalizowany i zoptymalizowany pod konkretne query. Dla restauracji trzymaliśmy prostą strukturę: { sku: string, available_quantity: number, unit: string, last_updated: timestamp }. To wystarczało, żeby wyświetlić listę produktów możliwych do zamówienia. Nie było tu informacji o partiach, o dokładnej lokalizacji w magazynie, o dostawcach. Te dane były niepotrzebne dla restauracji, więc nie obciążaliśmy nimi ani sieci, ani pamięci klienta.
Dla centrali projekcja była bogatsza: { sku: string, batches: [{ batch_id, quantity, expiry_date, location }], reserved: number, available: number }. Ta projekcja była używana przez magazynierów przy kompletacji zamówień.
Kluczowym problemem był initial state sync. Gdy restauracja łączyła się po raz pierwszy albo po długim offline, musiała dostać pełny snapshot aktualnego stanu, a nie tylko kolejne eventy. Zrobiliśmy to tak: przy połączeniu WebSocket klient wysyłał timestamp ostatniego znanego mu stanu (albo null przy pierwszym połączeniu). Serwer odsyłał albo pełny snapshot, albo tylko eventy od tego timestampu.
Brzmi prosto, ale tu się pojawiał edge case: klient wysyła timestamp T1, w międzyczasie serwer przetwarza event z timestampem T2, klient dostaje snapshot oparty o T1, po czym dostaje event T2, który już został uwzględniony w snapshot. Musieliśmy to obsłużyć idempotentnie – aplikacja eventu, który już jest uwzględniony w stanie lokalnym, nie może nic zepsuć. Każdy event miał unique ID, klient trzymał cache ostatnich N przetworzonych ID eventów i ignorował duplikaty.
"Większość problemów z synchronizacją stanów nie wynika z błędnych danych. Wynika z tego, że przez kilka sekund wszyscy mają rację jednocześnie, a produkcja musi później zdecydować, komu uwierzyć."
WebSocket dla push updates
Zdecydowaliśmy się na WebSocket zamiast polling, bo różnica w load na serwerze była dramatyczna. Przy pollingu co 5 sekund z 10 lokalizacji to 2 requesty na sekundę tylko dla check updates. Brzmi niewinnie, ale każdy request to połączenie, query do bazy albo Redis, response. WebSocket to jedno połączenie per klient i push tylko gdy faktycznie coś się zmieniło.
Użyliśmy Socket.io z prostych powodów: obsługa reconnect out of the box, fallback do long polling gdy WebSocket nie działa, łatwe roomsy. Każda lokalizacja dołączała do rooma odpowiadającego jej ID. Event dotyczący stanu magazynu był publikowany tylko do roomów tych lokalizacji, które powinny o nim wiedzieć. Nie było sensu informować restauracji A, że restauracja B coś zarezerwowała – chyba że to wpłynęło na globalną dostępność produktu.
Message format był minimalny. Zamiast przesyłać pełny stan po każdej zmianie, wysyłaliśmy deltę:
```javascript
{
type: 'stock_updated',
sku: 'PROD-123',
location_id: 'central_warehouse',
quantity_delta: -50,
available_after: 150,
event_id: 'evt_abc123',
timestamp: 1704067200
}
```
Klient aplikował deltę do swojego lokalnego stanu. Jeśli stanu nie miał (nie powinno się zdarzyć, ale zawsze zakładaj że się zdarzy), wysyłał request o pełny snapshot dla tego SKU.
Problem pojawił się z connection lifecycle. WebSocket to persistent connection, która może być przerwana w każdej chwili – zła sieć, restart serwera, timeout proxy. Musieliśmy założyć, że każde połączenie się kiedyś rozłączy i że reconnect musi być transparentny.
Socket.io radzi sobie z reconnect automatycznie, ale to nie wystarczy. Klient mógł być disconnected przez 30 sekund – w tym czasie mogło przyjść 20 eventów. Po reconnect serwer musiał wysłać te pominięte eventy. Zaimplementowaliśmy to jako event buffer w Redis: każdy event trafiał do Sorted Set z timestampem jako score, TTL 5 minut. Po reconnect klient wysyłał timestamp ostatniego przetworzonego eventu, serwer odsyłał wszystkie eventy od tego czasu z bufora.
Heartbeat był konieczny, żeby wykrywać „zombie connections” – klient myśli że jest połączony, serwer myśli że też, ale faktycznie pakiety nie dochodzą. Socket.io ma to wbudowane, ale musieliśmy odpowiednio skonfigurować timeouty. Zbyt krótki timeout powodował fałszywe disconnecty przy wolnej sieci. Zbyt długi sprawiał, że klient długo nie wiedział że jest offline. Wylądowaliśmy na 20 sekundach – ping co 10 sekund, timeout jeśli brak ponga przez 20 sekund.
Client-side state management
Po stronie klienta (PWA w restauracji albo aplikacja webowa w centrali) musieliśmy utrzymywać lokalny cache stanów magazynowych. To nie była tylko optymalizacja – to było wymaganie, żeby aplikacja działała płynnie i mogła przetrwać krótkie disconnections.
Cache był trzymany w IndexedDB, nie w pamięci. Pamięć się czyści przy refresh, a musieliśmy przetrwać przeładowanie aplikacji. IndexedDB dawało nam persistent storage z szybkim dostępem.
Struktura była prosta:
```javascript
{
sku: 'PROD-123',
available_quantity: 150,
unit: 'kg',
last_updated: 1704067200,
last_event_id: 'evt_abc123'
}
```
Każdy przychodzący event aktualizował ten rekord. Aplikacja delty była trywialna przy quantity_delta. Problem był przy konfliktach – co jeśli event przyszedł out of order? Używaliśmy timestampu jako arbitra – event ze starszym timestampem był ignorowany jeśli lokalny stan miał nowszy timestamp. To nie było idealne (zegary mogą się rozjechać), ale w praktyce działało, bo wszystkie timestampy pochodziły z serwera.
Większym problemem było optimistic UI. Restauracja dodaje produkt do zamówienia – chcemy natychmiast pokazać zmniejszoną dostępność, nie czekać na round-trip do serwera i z powrotem. Ale co jeśli serwer odrzuci rezerwację? Musieliśmy mieć mechanizm rollback.
Zrobiliśmy to przez pending operations queue. Gdy użytkownik zarezerwował towar, tworzyliśmy lokalną „pending reservation” ze statusem optimistic. Lokalny stan był od razu aktualizowany. Request szedł do serwera asynchronicznie. Jeśli serwer potwierdzał, pending operation była usuwana, a event z serwera tylko konsolidował stan. Jeśli serwer odrzucał, pending operation była rollbackowana, lokalny stan przywracany, użytkownik dostawał notyfikację.
Ten mechanizm sprawdził się też przy offline. Gdy połączenia nie było, operacje trafiały do kolejki z flagą offline. Gdy połączenie wracało, kolejka była przetwarzana. Dzięki idempotency keys po stronie serwera mogliśmy bezpiecznie retry – jeśli request się przebił mimo że myśleliśmy że nie, powtórne wysłanie nie zrobiło duplikatu.
Throttling i burst updates
Jeden z najbardziej irytujących problemów pojawił się przy bursts. Hala produkcyjna wysyłała 100 produktów do centralnego magazynu – to generowało 100 eventów StockReceived w ciągu kilku sekund. Wszystkie trafiały przez WebSocket do restauracji. Restauracja próbowała przetworzyć wszystkie, re-renderując UI po każdym. Efekt: UI zamrażał się na kilka sekund, procesor na 100%, bateria zżerana.
Musieliśmy wprowadzić throttling. Po stronie klienta nie procesowaliśmy każdego eventu osobno – zbieraliśmy je w batche przez 500ms, konsolidowaliśmy (jeśli przyszło 5 eventów dla tego samego SKU, aplikowaliśmy sumę delt), updatowaliśmy UI raz. Wizualnie nie było różnicy – użytkownik i tak nie zauważył różnicy między „update co 50ms” a „update co 500ms”. Ale UI pozostawał responsywny.
To stworzyło inny problem: podczas burstu lokalny stan mógł być chwilowo nieaktualny o tych 500ms buffering. Jeśli użytkownik w tym momencie próbował złożyć zamówienie oparte o ten stan, mógł operować na lekko stałych danych. Rozwiązaniem było flush bufora przed wysłaniem komendy do serwera – aplikowaliśmy wszystkie pending eventy, dopiero potem wysyłaliśmy komendę. To dawało gwarancję, że decyzja użytkownika była oparta o najnowszy znany stan.
"Na papierze WebSocket rozwiązuje problem opóźnień. W produkcji po prostu zamienia go na problem reconnectów, duplikatów i eventów, które akurat zniknęły w najmniej odpowiednim momencie. "
Horizontal scaling WebSocket
Przez pierwsze miesiące jeden node WebSocket wystarczał. Potem, przy rozbudowie sieci restauracji, zaczęliśmy skalować. Tu pojawił się klasyczny problem: mamy load balancer przed dwoma nodami WebSocket, klient łączy się do node A, event przychodzi do node B, jak zapewnić że klient dostanie ten event?
Socket.io ma adapter dla Redis, który rozwiązuje ten problem. Każdy node Socket.io jest połączony z tym samym Redis. Gdy node B dostaje event do opublikowania, wysyła go do Redis Pub/Sub. Wszystkie nody subskrybują ten kanał, dostają event, i przekazują go do swoich klientów w danym roomie. Klient połączony z node A dostanie event, mimo że przyszedł do node B.
To działało, ale stworzyło jeden edge case: event był publikowany do wszystkich nodów, ale tylko te, które miały klientów w danym roomie, wysyłały go dalej. To oznaczało, że Redis Pub/Sub dostawał wszystkie eventy, mimo że większość nodów większości eventów nie potrzebowała. Przy większej skali to mogło stać się bottleneckiem.
Rozwiązaniem było partycjonowanie roomów. Zamiast jednego kanału Redis Pub/Sub dla wszystkich eventów, mieliśmy kanał per lokalizacja. Node subskrybował tylko te kanały, które odpowiadały lokalizacjom jego aktualnie połączonych klientów. To wymagało dynamicznego subscribe/unsubscribe przy każdym connect/disconnect klienta, ale Redis radził sobie z tym bez problemu.
Konsekwencje eventual consistency
Eventual consistency brzmi teoretycznie, ale w praktyce oznacza konkretne sytuacje, które muszą być obsłużone przez logikę biznesową. Restauracja widzi dostępność 100 jednostek, zamawia 80, w międzyczasie inna restauracja zarezerwowała 50. Pierwsze zamówienie zostanie częściowo zrealizowane – dostępne jest tylko 50. System musi to obsłużyć gracefully, nie crashować, nie tworzyć niespójności w danych.
Zaimplementowaliśmy partial fulfillment. Zamówienie miało trzy statusy: requested (złożone), allocated (zasoby przydzielone), fulfilled (zrealizowane). Przy alokacji system próbował przydzielić pełną ilość. Jeśli nie było wystarczającej dostępności, przydzielał tyle ile było, resztę oznaczał jako backorder. Restauracja dostawała notyfikację: „Zamówienie częściowo zrealizowane, 50 jednostek dostępne natychmiast, 30 jednostek w backorder, oczekiwany termin realizacji: 3 dni”.
To wymagało edukacji użytkowników. Na początku były skargi: „system pokazywał że jest, a nie ma”. Musieliśmy wytłumaczyć, że dostępność to snapshot z konkretnego momentu, może się zmienić w ciągu kilku sekund, szczególnie gdy wszystkie restauracje zamawiają jednocześnie. Dodaliśmy do UI timestamp „dane aktualne na” i indicator czy połączenie WebSocket jest aktywne. Gdy nie było połączenia, pokazywaliśmy warning: „Dane mogą być nieaktualne, pracujesz offline”.
Wprowadziliśmy też soft reservation przy dodawaniu do koszyka. Gdy restauracja dodawała produkt do zamówienia (ale jeszcze go nie potwierdzała), system tworzył tymczasową rezerwację z TTL 15 minut. Ta rezerwacja zmniejszała dostępność widoczną dla innych restauracji. Jeśli po 15 minutach zamówienie nie było potwierdzone, rezerwacja wygasała, dostępność wracała. To zredukowało sytuacje „dodałem do koszyka, poszedłem po kawę, wróciłem, potwierdzam, a już nie ma”.
Conflict detection i resolution
Mimo eventual consistency musieliśmy wykrywać i rozwiązywać konflikty. Kluczowe było robienie tego asynchronicznie, nie blokując write path. Gdy system wykrył konflikt (np. suma rezerwacji przekracza fizyczny stan), tworzył rekord w conflicts table z opisem problemu i statusem pending_resolution.
Background job przetwarzał te konflikty według reguł biznesowych. Prosty przykład: dwie restauracje zarezerwowały ten sam towar, suma przekracza dostępność o 10 jednostek. Reguła: priorytet według czasu złożenia zamówienia. System redukował młodsze zamówienie o nadwyżkę, tworzył backorder, wysyłał notyfikację.
Bardziej złożone konflikty trafiały do ręcznego rozwiązania przez managera magazynu. System pokazywał w panelu admin listę pending conflicts z kontekstem: które restauracje, jakie produkty, jaka różnica. Manager mógł zdecydować: anulować jedno zamówienie, uruchomić dodatkową produkcję, przesunąć termin, etc.
Kluczowe było to, że konflikty nie blokowały systemu. Operacje były przyjmowane, przetwarzane, conflict detection następował asynchronicznie. To pozwalało utrzymać throughput nawet przy częstych konfliktach w godzinach szczytu.
Monitoring i observability
Bez odpowiedniego monitoringu eventual consistency to czarna skrzynka. Nie wiesz, czy system działa poprawnie, czy dane są spójne, czy klienci dostają updaty.
Kluczowe metryki:
- Event lag - różnica między timestampem eventu a czasem jego przetworzenia przez projekcję. Target: median poniżej 100ms, p99 poniżej 5 sekund.
- WebSocket connection count per node - żeby wykrywać nierównomierne rozłożenie load.
- Reconnection rate - częste reconnecty to symptom niestabilnej sieci albo źle skonfigurowanych timeoutów.
- Message delivery latency - czas między publish eventu a delivery do klienta. Target: median poniżej 200ms.
- Pending operations queue depth - ile operacji offline czeka na sync. Jeśli rośnie, coś jest nie tak z connectivity albo throughput serwera.
Alert na excessive lag był krytyczny. Jeśli event lag przekraczał 10 sekund, to oznaczało, że projekcje są znacząco nieaktualne, restauracje widzą stale dane, ryzyko konfliktów rośnie. Najczęstsza przyczyna: problem z brokerem eventów albo spike load na serwerze przetwarzającym eventy.
Dodaliśmy też custom metric: consistency score. Co minutę background job porównywał stan write modelu (faktyczny stan w bazie magazynowej) ze stanami read projections (cache w Redis per lokalizacja). Różnice były akceptowalne przez krótki czas, ale jeśli stan projekcji był nieaktualny o więcej niż 1 minutę, to był problem. Ten metric pozwalał nam wykryć sytuacje, gdzie eventy były publikowane, ale projekcje nie były aktualizowane – np. przez bug w handlerze eventów.
"Monitoring distributed systemów to moment, w którym odkrywasz, że większość dashboardów pokazuje jedynie elegancką wizualizację problemów, o których użytkownicy zdążyli już poinformować support. "
Fallback strategies
WebSocket to nie jest niezawodne połączenie. Proxy mogą je timeoutować, niektóre sieci korporacyjne je blokują, problem może być po stronie serwera. Musieliśmy mieć fallback.
Socket.io automatycznie fallbackuje do long polling, ale to nie wystarcza. Long polling to wciąż wychodzące połączenia z klienta, niektóre firewalle je blokują. Ostatecznym fallbackiem był prosty polling: co 30 sekund request o aktualny stan. Wolniejsze, mniej efektywne, ale zawsze działało.
Testowaliśmy też Server-Sent Events jako alternatywę. SSE to prostsza technologia niż WebSocket – tylko server-to-client push, klient komunikuje się normalnym HTTP. Ma plusy: działa przez każdy HTTP proxy, automatyczny reconnect w przeglądarce. Minus: tylko jednokierunkowe, więc komendy od klienta muszą iść normalnym POST.
Ostatecznie zostaliśmy przy WebSocket + Socket.io fallback, bo dla naszego case’u dwukierunkowa komunikacja była wartościowa. Ale SSE byłoby dobrym wyborem dla prostszego scenariusza, gdzie klient tylko słucha update’ów.
Lessons learned
Po roku działania systemu produkcyjnego widzieliśmy gdzie były nasze błędne założenia. Największym było myślenie, że eventual consistency to tylko techniczny detal implementacji. To nie jest – to fundamentalna charakterystyka systemu, która wpływa na logikę biznesową, UX, komunikację z użytkownikami, proces operacyjny.
Musieliśmy zmienić procesy w restauracjach. Zamiast „zamów ile potrzebujesz”, stało się „zamów wcześniej, bo dostępność się zmienia”. Zamiast „jak widzisz że jest, to jest”, stało się „jak widzisz że jest, to prawdopodobnie jest, ale potwierdź”.
Edukacja użytkowników była trudniejsza niż implementacja techniczna. Próby tłumaczenia eventual consistency managerom restauracji kończyły się pomieszaniem. Co zadziałało: konkretne zasady bez technicznych detali. „Dostępność aktualizuje się co kilka sekund”, nie „projekcja read modelu jest eventual consistent z write modelem”. „Dodanie do koszyka rezerwuje towar na 15 minut”, nie „tworzymy soft reservation z TTL”.
Monitoring był absolutnie krytyczny. Bez visibility na lag, connection status, conflict rate, lecieliśmy po omacku. Wiele bugów wykryliśmy dzięki metrykom zanim użytkownicy się zorientowali. Inwestycja w observability zwróciła się w pierwszym miesiącu.
Największym sukcesem było to, że system przetrwał spike load w Black Friday. 10 restauracji zamawiających jednocześnie setki pozycji – zero downtime, zero data loss, konflikty wykryte i rozwiązane automatycznie. Read projections miały lag do 2 sekund w szczycie, ale to było w ramach SLA. Użytkownicy zgłosili tylko kilka przypadków częściowego fulfillment, co było akceptowalne.
Gdybym projektował to od nowa, zmieniłbym jedno: więcej czasu na testowanie network failures na początku. Większość problemów, które przeżyliśmy w pierwszych miesiącach, była związana z disconnections, timeoutami, reconnectami. Mieliśmy unit testy, integration testy, load testy, ale nie mieliśmy chaos testing – celowego odcinania połączeń, restartowania nodów w losowych momentach, symulowania lag. Taki testing wykryłby dużo błędów przed produkcją.
Rozproszony system magazynowy to nigdy nie jest „zaimplementowaliśmy i działa”. To ciągła walka z network failures, race conditions, edge cases. Ale przy odpowiedniej architekturze – event-driven, CQRS, eventual consistency z jasno zdefiniowanym SLA – można zbudować coś, co faktycznie pracuje w produkcji pod realnym obciążeniem. Tylko nie licz, że „real-time” będzie oznaczać dokładnie to, co myślałeś na początku.
Podsumowanie
Eventual consistency brzmi jak detal architektoniczny, dopóki pierwsze dwa zamówienia nie zaczną walczyć o ten sam zapas. Wtedy okazuje się, że większość systemu nie polega na synchronizacji danych, tylko na akceptowaniu faktu, że przez krótką chwilę każdy patrzy na trochę inną wersję rzeczywistości.
System działał najlepiej nie wtedy, gdy próbowaliśmy wymusić idealną spójność, ale wtedy, gdy nauczyliśmy się żyć z jej brakiem. Produkcyjny ruch i tak znajdzie moment, w którym event przyjdzie za późno, klient zdąży kliknąć za szybko, a stan magazynowy okaże się prawdziwy tylko przez bardzo krótki i bardzo optymistyczny okres.
Zobacz powiązane case studies i analizy
Procesy, architektura i workflow powiązane z tematami poruszanymi w tym materiale – od integracji i realtime systems po automatyzacje operacyjne.