W świecie mikroserwisów jednym z największych wyzwań jest zapewnienie spójności danych w systemach rozproszonych. Kluczowe jest, aby nawet w przypadku awarii pojedynczego komponentu cały system był w stanie odzyskać stabilność i nie pozostawić klienta w niejednoznacznej sytuacji. Saga pattern, czyli wzorzec sag, to jedno z najlepszych rozwiązań pozwalające radzić sobie z problemami rozproszonych transakcji. Dzięki niemu, zamiast jednej, niedzielonej transakcji, system realizuje szereg mniejszych, lokalnych transakcji z opcjonalnymi akcjami kompensacyjnymi, umożliwiającymi odwrócenie skutków w razie błędów.
Przykładem zastosowania wzorca sag może być aplikacja e-commerce, gdzie złożenie zamówienia skutkuje szeregiem operacji takich jak obciążenie karty płatniczej klienta, rezerwacja towaru w magazynie i przygotowanie wysyłki. Jeśli na którymkolwiek z tych etapów pojawi się błąd — na przykład brak środków na koncie — system musi być w stanie niezwłocznie cofnąć wcześniejsze operacje. Saga umożliwia to dzięki „kompensującym” metodom, które przywracają poprzedni stan (np. anulują zamówienie lub zwalniają zarezerwowane produkty).
Implementację tego wzorca można zrealizować na dwa główne sposoby: choreografii lub orkiestracji. W choreografii mikroserwisy komunikują się między sobą za pomocą zdarzeń publikowanych do brokera wiadomości (np. Apache Kafka), reagując autonomicznie na informacje od innych usług. Jest to podejście zdecentralizowane, które dobrze sprawdza się w systemach opartych na zdarzeniach. Z kolei orkiestracja opiera się na centralnym kontrolerze — tzw. satrapie sag — który zarządza całym przepływem, wysyła polecenia do odpowiednich usług i zbiera informacje o ich sukcesach lub porażkach.
W praktycznym przykładzie wykorzystującym NestJS, KafkaJS i TypeScript można stworzyć kompletną architekturę składającą się z kilku mikroserwisów: zamówień, płatności, magazynu oraz centralnego orkiestratora sag. Każdy z tych mikroserwisów jest napisany w stylu frameworka NestJS i komunikuje się z pozostałymi poprzez zdarzenia przesyłane przez Apache Kafka, który działa jako centralny broker.
Zaczynamy od stworzenia środowiska — za pomocą pliku docker-compose konfigurujemy kontenery z Kafka i Zookeeperem. Następnie wykorzystując CLI NestJS, generujemy cztery mikroserwisy: order-service, payment-service, inventory-service oraz saga-orchestrator. Każdy z nich rejestruje własnego konsumenta Kafka i produkuje zdarzenia adekwatne do swojej logiki biznesowej. Na przykład, order-service po otrzymaniu polecenia stworzenia zamówienia emituje event order.created, który następnie przejmuje magazyn (inventory-service), rezerwujący produkt. Po potwierdzeniu rezerwacji, inicjowana jest płatność, a pozytywnie zakończony proces oznacza zakończenie sagi.
Kluczowym komponentem całej infrastruktury jest orchestrator, który odpowiada za kontrolę stanu transakcji. To on rozpoczyna proces, słucha zdarzeń i uruchamia akcje naprawcze jeśli któryś ze składników systemu zawiedzie. W przypadku nieudanej płatności lub braku towaru orchestrator zajmuje się cofnięciem wcześniejszych kroków. Wszystko odbywa się asynchronicznie i niezależnie od siebie, co pozwala systemowi działać płynnie nawet w obliczu lokalnych problemów.
Wysłanie zapytania HTTP GET do endpointu /saga/start/12345 inicjuje przykład zamówienia z ID 12345. Następnie, na podstawie zdarzeń Kafka, orchestrator prowadzi cały przebieg procedury, kierując komunikację między usługami. Możemy podejrzeć przebieg komunikacji w logach lub z użyciem narzędzi CLI Kafki, aby upewnić się, że zostały utworzone odpowiednie topiki, a dane rzeczywiście przepływają między komponentami zgodnie z założonym flow.
Cała implementacja pokazuje, że z pomocą NestJS i Kafki można zbudować efektywny system mikroserwisów, odporny na błędy i zdolny do samonaprawy bez konieczności ręcznego interweniowania w błędy logiczne. Dzięki proceduralnemu podziałowi transakcji i celowemu wykorzystaniu wzorców projektowych takich jak saga, aplikacje szybciej reagują, lepiej się skalują i są łatwiejsze w utrzymaniu.
Najważniejsze zalety zastosowania wzorca sag w omawianym podejściu to:
– Efektywne zarządzanie transakcjami rozproszonymi bez potrzeby wykorzystywania mechanizmów blokujących typu 2PC (two-phase commit).
– Możliwość cofnięcia pojedynczych kroków w razie błędu (kompensacja) i zachowania spójności systemu.
– Asynchroniczna komunikacja mikroserwisów z użyciem brokera zdarzeń, co zwiększa wydajność i elastyczność architektury.
– Przejrzystość procesu dzięki centralnemu orchestratorowi, który pełni rolę kontrolera stanu i może służyć jako miejsce obserwacji całej logiki transakcji.
Ten projekt jest świetnym punktem wyjścia do nauki o tworzeniu odpornych mikroserwisów z wykorzystaniem technologii NestJS, Kafka oraz TypeScript. Dzięki odpowiedniemu podejściu do struktury zdarzeń i testom można stworzyć system, który nie tylko jest nowoczesny i wydajny, ale przede wszystkim — bezpieczny i stabilny.
Pełny kod źródłowy można znaleźć na GitHubie autora, co ułatwia adaptację rozwiązania do własnych projektów i eksperymentowanie z własnymi wariantami wdrożenia.