Saga – wzorzec w architekturze mikroserwisów – czyli Spring Boot i Apache Kafka w praktyce.
W architekturze mikroserwisowej z reguły obowiązuje zasada „Database per Service”, czyli oddzielna baza danych dla każdej usługi. Dzięki temu każdy mikroserwis ma pełną kontrolę nad swoimi danymi i może używać dowolnej technologii bazy danych, a awarie jednego serwisu nie psują danych innych.
Jednak podejście to rodzi wyzwanie: jak zachować spójność danych w procesach biznesowych obejmujących wiele serwisów? Skoro każda usługa ma osobną bazę, nie da się użyć zwykłej transakcji ACID obejmującej wszystkie bazy jednocześnie.
Klasyczne Two-Phase Commit (2PC) – protokół dwufazowego zatwierdzania – teoretycznie pozwala na taką rozproszoną transakcję, ale w świecie mikroserwisów jest rzadko stosowany. 2PC wymaga bowiem koordynatora i blokuje zasoby w wielu bazach; jest skomplikowany, obniża dostępność i nie zawsze jest wspierany przez różne silniki baz danych. Z pomocą przychodzi nam wzorzec Saga – rozwiązanie, które umożliwia realizację transakcji rozproszonych bez globalnej transakcji 2PC.
Spis treści
Czym jest wzorzec Saga?
Saga to sekwencja lokalnych transakcji wykonywanych kolejno w różnych mikroserwisach, które wspólnie realizują jeden proces biznesowy.
Każdy etap Sagi to zwykła transakcja w ramach jednej bazy danych (np. zapis w swojej tabeli), po której serwis publikuje zdarzenie (event) lub komunikat – sygnał do kolejnego kroku w innym serwisie.
Jeśli któryś z etapów się nie powiedzie (np. któryś serwis nie może wykonać swojej operacji z powodu naruszenia reguł biznesowych lub błędu), Saga uruchamia transakcje kompensacyjne, które cofają skutki wcześniejszych operacji, przywracając system do spójnego stanu. W efekcie otrzymujemy mechanizm podobny do rollbacku w klasycznej transakcji, ale realizowany na poziomie aplikacji – poprzez dodatkowe akcje odwracające.
Kluczowe cechy wzorca Saga
- Lokalne transakcje – Każdy serwis wykonuje swoją część pracy niezależnie, używając własnej bazy i zapewniając lokalną atomowość i integralność danych.
- Komunikacja asynchroniczna – Po zakończeniu swojej transakcji, serwis informuje inne komponenty wysyłając komunikat/zdarzenie (np. przez Apache Kafka, RabbitMQ itp.). To zdarzenie wyzwala kolejny krok Sagi w innym serwisie.
- Brak globalnej transakcji – Nie ma jednego globalnego mechanizmu blokującego zasoby we wszystkich bazach. Każdy etap zatwierdza zmiany od razu lokalnie, więc nie występuje problem blokad i oczekiwania na commit wielu baz naraz.
- Kompensacje zamiast rollbacku – Ponieważ zmiany są zatwierdzane na bieżąco, ewentualne błędy wymagają ręcznego cofnięcia wcześniejszych operacji poprzez wykonanie odwrotnej akcji (np. anulowanie płatności, usunięcie utworzonej rejestracji itp.). Programista musi zaprojektować takie operacje kompensujące – nie dzieją się one automatycznie jak rollback w bazie.
- Spójność ostateczna (ang. eventual consistency) – Saga zapewnia, że wszystkie serwisy prędzej czy później uzyskają spójny stan danych, ale nie gwarantuje, że w każdej chwili widoczny stan jest spójny (czyli dopuszczamy chwilowe stany pośrednie). To tzw. eventual consistency – ostateczna spójność danych po zakończeniu całej sekwencji.
Podsumowując, Saga pozwala zachować spójność danych w rozproszonym systemie bez potrzeby użycia 2PC. Zamiast jednego wielkiego „commit” na końcu, mamy wiele mniejszych commitów w trakcie procesu, a ewentualne błędy korygujemy dodatkowymi transakcjami kompensującymi. Dzięki temu mikroserwisy pozostają luźno powiązane (komunikują się komunikatami), a cały system unika wad protokołu 2PC (np. blokowania zasobów i pojedynczego punktu awarii w postaci koordynatora).
Saga vs Two-Phase Commit (2PC) – dlaczego Saga wygrywa w mikroserwisach?

Źródło: microservices.io
Na schemacie porównano tradycyjną rozproszoną transakcję 2PC (u góry) z podejściem Saga (na dole). W 2PC koordynator zarządza jednoczesnym zatwierdzeniem w wielu serwisach, natomiast Saga dzieli proces na serię niezależnych operacji lokalnych, przekazując sterowanie między serwisami za pomocą komunikatów.
W modelu 2PC jeden koordynator najpierw pyta wszystkie uczestniczące serwisy/bazy, czy są gotowe zatwierdzić transakcję (faza prepare), a potem wydaje polecenie commit, jeśli wszyscy byli gotowi (faza commit). Jeśli choć jeden zgłosi problem w fazie przygotowania, koordynator zleca rollback we wszystkich – cała transakcja zostaje anulowana.
W teorii brzmi dobrze, ale w praktyce w środowisku mikroserwisów 2PC nie jest już taki prosty w implementacji:
- Nie ma wspólnego mechanizmu transakcji dla różnych technologii baz (np. miks SQL i NoSQL).
- Koordynator staje się wąskim gardłem i potencjalnym pojedynczym punktem awarii (ang. single point of failure).
- W przypadku awarii lub opóźnień, uczestnicy mogą długo trzymać blokady na danych czekając na decyzję, co ogranicza skalowalność.
- Implementacja 2PC w obrębie aplikacji jest złożona i podatna na błędy, szczególnie gdy serwisy są rozproszone geograficznie lub sieć jest zawodna.
Saga rozwiązuje te problemy przez rezygnację z globalnej atomowości na rzecz ostatecznej spójności. Każdy serwis od razu finalizuje swoją zmianę (nie czeka na inne serwisy), więc brak globalnych blokad. Koordynacja odbywa się przez wymianę komunikatów/asynchronicznych zdarzeń, np. za pomocą Apache Kafka. Nie potrzebujemy centralnego menedżera transakcji w tradycyjnym sensie – logika kontroli przepływu jest wbudowana w samą aplikację (o podejściach do tej kontroli zaraz powiemy). W razie problemów, Saga zapewnia ręczne odkręcenie (kompensacja) już zatwierdzonych kroków.
Owszem, wymaga to więcej kodu (trzeba przewidzieć i obsłużyć scenariusze kompensacji), ale upraszcza architekturę i zwiększa niezawodność w środowisku rozproszonym. Z punktu widzenia spójności danych, Saga gwarantuje rezultat równoważny 2PC (wszystko albo nic), ale osiągany inną drogą – sekwencją akcji i kompensacji zamiast jednego globalnego commit/rollback.
Uwaga: Warto pamiętać, że Saga nie zapewnia izolacji transakcji w rozumieniu ACID. Równoległe wykonywanie wielu Sag może prowadzić do konfliktów (np. dwa procesy jednocześnie rezerwujące te same zasoby). Programista musi sam zadbać o mechanizmy zapobiegające anomaliom, np. poprzez odpowiednie blokady logiczne czy sprawdzanie wersji danych. Mimo to Saga pozostaje de facto standardem utrzymania spójności danych w świecie mikroserwisów, podczas gdy 2PC jest traktowane jako ostateczność (lub materiał teoretyczny).
Choreografia vs orkiestracja – dwie szkoły implementacji Sag
Wzorzec Saga określa co ma być zrobione (podział transakcji na kroki i kompensacje), ale pozostawia swobodę jak to zaimplementować.
W praktyce spotykamy dwa style koordynacji sag: choreografię oraz orkiestrację. Oba podejścia osiągają ten sam cel, ale różnią się sposobem, w jaki poszczególne serwisy dowiadują się, co mają robić w ramach procesu.
Przyjrzyjmy się im bliżej.
Saga w podejściu choreografii ( ang. choreography)
Choreografia opiera się w 100% na komunikacji asynchronicznej za pomocą zdarzeń domenowych. Nie ma centralnego „reżysera” procesu – każdy mikroserwis sam wie, jakie akcje podjąć w reakcji na dane zdarzenie i jakie zdarzenia wyemitować po wykonaniu swojej pracy. Mówiąc obrazowo, serwisy tańczą ze sobą „w rytmie zdarzeń”, reagując na siebie nawzajem.
Jak to wygląda w praktyce? Rozważmy prosty scenariusz biznesowy systemu szkoleń: zapis kursanta na szkolenie, które wymaga opłaty i ma ograniczoną liczbę miejsc. Załóżmy, że mamy trzy mikroserwisy:
- Serwis Rejestracji – przyjmuje zgłoszenie na szkolenie od kursanta (np. poprzez REST API) i zapisuje w swojej bazie wstępną rejestrację.
- Serwis Płatności – obsługuje płatność za szkolenie.
- Serwis Szkoleń – zarządza danymi o szkoleniach i miejscami na kursach.
W podejściu choreografii przebieg Saga (w wariancie happy path, czyli bez błędów) mógłby być następujący:
- Rejestracja kursanta – Serwis Rejestracji otrzymuje żądanie zapisu na szkolenie. Tworzy w swojej bazie wpis rejestracji ze statusem np.
PENDING
(oczekujące) i publikuje zdarzenieEnrollmentCreated
(rejestracja utworzona). - Płatność – Serwis Płatności odbiera zdarzenie
EnrollmentCreated
(np. z topicu Kafkienrollment-events
). Na jego podstawie rozpoczyna proces opłacenia szkolenia przez kursanta. Jeśli płatność się powiedzie, zapisuje w swojej bazie transakcję i wysyła zdarzeniePaymentCompleted
(płatność zakończona). Jeśli się nie powiedzie – publikuje zdarzeniePaymentFailed
(płatność nieudana). - Reakcja na płatność – Zdarzenie
PaymentCompleted
trafia do zainteresowanych serwisów. Serwis Rejestracji może je odebrać i zaktualizować status rejestracji naPAID
(opłacone). Jednocześnie (równolegle) Serwis Szkoleń również słucha tego zdarzenia – widząc informację o opłaconej rejestracji, próbuje zarezerwować kursantowi miejsce na wybranym szkoleniu (lokalna transakcja w bazie szkoleniowej). Następnie Serwis Szkoleń publikuje zdarzenieSeatReserved
(miejsce zarezerwowane) alboSeatReservationFailed
(brak miejsca). - Finalizacja lub kompensacja – Załóżmy, że miejsce udało się zarezerwować –
SeatReserved
zostaje odebrane przez Serwis Rejestracji, który zmienia status rejestracji naCONFIRMED
(potwierdzone) i np. emituje zdarzenieEnrollmentCompleted
(zapis ukończony), co może posłużyć innym usługom (np. serwisowi powiadomień, który wyśle e-mail z potwierdzeniem). W ten sposób Saga się kończy sukcesem.
A co jeśli któregoś kroku nie da się wykonać? Np. jeśli Serwis Szkoleń opublikujeSeatReservationFailed
(brak miejsc). W choreografii pozostałe serwisy muszą same zareagować na ten fakt. Serwis Rejestracji mógłby w takiej sytuacji oznaczyć rejestrację jako anulowaną (CANCELLED
), a Serwis Płatności – po otrzymaniu informacji o nieudanej rezerwacji – mógłby automatycznie zainicjować zwrot płatności (np. publikując zdarzeniePaymentRefunded
). Każdy serwis zna logikę kompensacji swojej części: Rejestracja usuwa/znakuje wpis, Płatność robi zwrot, itp. Po wykonaniu kompensacji Saga również się kończy (ostatecznie kursant nie jest zapisany, a pieniądze wróciły – układ wrócił do stanu sprzed procesu).
Widzimy, że w podejściu choreograficznym cała sekwencja jest kontrolowana implicite przez samą kolejność zdarzeń. Zalety tego podejścia to m.in. prostota integracji: dodanie nowego kroku (np. nowego serwisu wysyłającego powiadomienia) często wymaga tylko dołączenia nowego „tancerza do baletu” – nowy serwis zaczyna nasłuchiwać odpowiedniego zdarzenia i reagować, nie trzeba modyfikować centralnej logiki sterującej. Choreografia jest też naturalnie skalowalna i luźno powiązana – serwisy nie wywołują się nawzajem bezpośrednio, komunikują się przez brokera (Kafka), co zmniejsza zależności.
Jednak choreografia ma też wady. Przy rozbudowanych procesach biznesowych kolejka zdarzeń może stać się trudna do prześledzenia – logika jest rozsiana po wielu usługach. Trudniej zrozumieć całościowy obraz, co może utrudnić debugowanie i monitorowanie. W przypadku złożonych kompensacji (wielu rzeczy do odwołania w razie błędu) rozproszona natura choreografii bywa kłopotliwa – trzeba dopilnować, by każdy serwis zareagował poprawnie na zdarzenia błędów. Istnieje ryzyko powstania „tańca chaosu” – gdy logika zależności między zdarzeniami staje się zawiła jak spaghetti. Mimo to, do prostszych procesów lub gdy zależy nam na maksymalnym luzie między serwisami, choreografia bywa wystarczająca.
Zobaczmy fragment kodu obrazujący podejście choreograficzne z użyciem Spring Boot i Kafki. Poniżej przykładowa implementacja obsługi zdarzenia utworzenia rejestracji oraz wysłania zdarzenia płatności (krok 1-2 z naszego scenariusza):
// Serwis Rejestracji – publikacja zdarzenia po zapisaniu rejestracji @Service public class EnrollmentService { @Autowired private KafkaTemplate<String, Object> kafkaTemplate; public void registerStudent(Long studentId, Long trainingId) { // ... (logika zapisania rejestracji w lokalnej bazie, status PENDING) EnrollmentCreatedEvent event = new EnrollmentCreatedEvent(studentId, trainingId); kafkaTemplate.send("enrollment-events", event); // Zdarzenie 'EnrollmentCreated' zostanie odebrane przez serwis płatności } }
// Serwis Płatności – nasłuchiwanie zdarzenia rejestracji i wykonanie płatności @Service public class PaymentListener { @Autowired private KafkaTemplate<String, Object> kafkaTemplate; @KafkaListener(topics = "enrollment-events", groupId = "payment-service") public void onEnrollmentCreated(EnrollmentCreatedEvent event) { // Po otrzymaniu zdarzenia, rozpoczynamy proces płatności boolean paid = paymentProcessor.processPayment(event.getStudentId(), event.getTrainingId()); if (paid) { // emitujemy zdarzenie o zakończonej płatności PaymentCompletedEvent payEvent = new PaymentCompletedEvent(event.getStudentId(), event.getTrainingId()); kafkaTemplate.send("payment-events", payEvent); } else { // emitujemy zdarzenie o nieudanej płatności (do ewentualnej kompensacji) PaymentFailedEvent failEvent = new PaymentFailedEvent(event.getStudentId(), event.getTrainingId()); kafkaTemplate.send("payment-events", failEvent); } } }
Powyższy kod ilustruje dwa pierwsze etapy Saga w trybie choreografii. Gdy Serwis Rejestracji utworzy nową rejestrację, publikuje zdarzenie EnrollmentCreatedEvent
na temat (topic) enrollment-events
. Serwis Płatności (nasłuchujący ten topic dzięki adnotacji @KafkaListener
) otrzymuje to zdarzenie i wykonuje logikę opłacenia – tutaj symulowaną przez metodę paymentProcessor.processPayment(...)
. W zależności od wyniku, publikuje kolejne zdarzenie: PaymentCompletedEvent
albo PaymentFailedEvent
na topic payment-events
. Dalej w Saga kolejne serwisy (np. Serwis Szkoleń) będą nasłuchiwać payment-events
i tak dalej. W ten sposób cały przepływ jest napędzany zdarzeniami i żaden centralny komponent nie dyktuje kolejności – każda usługa reaguje autonomicznie.
Saga w podejściu orkiestracji (orchestration)
Drugim podejściem do implementacji Saga jest orkiestracja, gdzie wprowadzamy dedykowany komponent pełniący rolę koordynatora (orkiestratora) całego procesu. Można go sobie wyobrazić jako maestro dyrygującego orkiestrą – jeden centralny serwis wie, jakie kroki powinny nastąpić po kolei i wysyła wytyczne (komendy) do poszczególnych mikroserwisów, czekając na ich odpowiedź. To podejście bywa też nazywane wzorcem Process Manager lub po prostu Saga Orchestrator.
W naszym przykładzie z zapisami na szkolenie moglibyśmy wydzielić np. Serwis Orkiestratora, który koordynuje współpracę pozostałych serwisów (Rejestracji, Płatności, Szkoleń). Przebieg Saga w wariancie orkiestracji (happy path) wyglądałby tak:
- Start Saga – Serwis Rejestracji przyjmuje żądanie zapisu kursanta (tak jak wcześniej), tworzy wpis
PENDING
w swojej bazie ale nie wysyła od razu zdarzenia do wszystkich. Zamiast tego powiadamia Orkiestratora (np. wywołuje metodę orkiestratora lub publikuje specjalny komunikat-komendę). Orkiestrator rozpoczyna nową Sagę, zapisując sobie jej kontekst (np. ID rejestracji, ID kursanta i szkolenia) i przechodzi do kroku 2. - Płatność (komenda) – Orkiestrator wysyła komunikat typu komenda do Serwisu Płatności, np.
ProcessPaymentCommand
z informacją kogo i za co obciążyć. (W praktyce w świecie Kafki może to być opublikowanie wiadomości na dedykowanym topicu, powiedzmypayment-commands
, na którym słucha Serwis Płatności). Następnie orkiestrator czeka na wynik. - Płatność (wynik) – Serwis Płatności otrzymuje komendę, wykonuje płatność i odsyła odpowiedź – np. publikuje zdarzenie
PaymentCompleted
lubPaymentFailed
na inny topic (np.payment-events
). Orkiestrator nasłuchuje tych odpowiedzi. Gdy otrzyma wynik płatności, aktualizuje stan Saga (np. odnotowuje “zapłacone” lub “błąd płatności”) i decyduje co dalej. - Rezerwacja miejsca (komenda) – Jeśli płatność się powiodła, orkiestrator wysyła kolejną komendę, tym razem do Serwisu Szkoleń, np.
ReserveSeatCommand
z informacją o kursancie i szkoleniu. Serwis Szkoleń próbuje zarezerwować miejsce i odsyła wynik –SeatReserved
lubSeatReservationFailed
. - Zakończenie Saga – Orkiestrator odbiera wynik rezerwacji. Jeśli miejsce zarezerwowano (
SeatReserved
), może uznać Sagę za zakończoną sukcesem – wówczas np. powiadamia Serwis Rejestracji, wysyłając mu komendęConfirmEnrollment
(aby ten zmienił status rejestracji na potwierdzony), po czym publikuje ewentualne finalne zdarzenieEnrollmentCompleted
(do którego mogą się podpiąć inne usługi, np. powiadomień). Jeśli jednak otrzyma informację o braku miejsca (SeatReservationFailed
), inicjuje kompensacje: np. wysyła komendęCancelPayment
do Serwisu Płatności (żądanie zwrotu pieniędzy) iCancelEnrollment
do Serwisu Rejestracji (anulowanie rejestracji). Gdy te zostaną wykonane, Saga kończy się, mając pewność, że wcześniejsze części procesu zostały odwrócone.
Podejście orkiestracji centralizuje logikę sterowania w jednym miejscu.
Zalety takiego rozwiązania są następujące:
- Cały przepływ procesu jest jasno zdefiniowany w kodzie orkiestratora – łatwiej go śledzić, debugować i testować. Możemy np. logować każdy krok, wiedzieć dokładnie na którym etapie jest Saga.
- Skomplikowane scenariusze błędów i kompensacji są prostsze do zaimplementowania, bo jedna jednostka (orkiestrator) wie, co już zrobiono, a co trzeba cofnąć. Nie musimy polegać na tym, że każdy serwis osobno „domyśli się” jak zareagować – orkiestrator wydaje polecenia kompensacji świadomie.
- Możemy łatwo wprowadzać bardziej złożone reguły decyzyjne – np. jeśli płatność nie przeszła, spróbować zapisać to w logu i ponowić za 15 minut itp. – bo mamy centralny punkt, który może ogarnąć takie scenariusze.
Oczywiście są i wady:
- Orkiestrator to dodatkowy komponent, który trzeba napisać i utrzymywać. Wprowadza pewne sztywne powiązanie – wie o istnieniu innych serwisów i rodzaju akcji, jakie można w nich wykonać (np. musi znać komendę
ReserveSeat
). Dodanie nowego kroku (np. powiadomienie e-mail) oznacza modyfikację kodu orkiestratora (i potencjalnie obsługi komendy w nowym serwisie). - Orkiestrator staje się centralnym elementem procesu – jeśli on zawiedzie, cała Saga może utknąć. Trzeba zadbać o wysoką dostępność orkiestratora i mechanizmy odporności (np. ponawianie niedostarczonych komend, odczytywanie nieprzetworzonych zdarzeń po restarcie itp.). W literaturze wspomina się, że orkiestrator to potencjalny single point of failure (choć można go nadmiarowo zreplikować).
- Nieumiejętne użycie orkiestracji może prowadzić do mini-monolitu – czyli zbytniego skupienia logiki w jednym miejscu. Trzeba balansować między tym, co koordynuje orkiestrator, a co wciąż samodzielnie robią serwisy.
Implementacja Spring Boot + Kafka
Orkiestrator może komunikować się z serwisami różnymi sposobami. Czasem używa się synchronicznych wywołań REST (np. orchestrator wywołuje endpoint płatności i czeka na response), ale takie podejście częściowo niweluje zalety asynchroniczności. Skoro i tak mamy Apache Kafka, pokażemy wariant w pełni asynchroniczny, gdzie komendy i wyniki są przesyłane jako komunikaty na topicach.
Możemy np. ustalić, że:
- Orkiestrator publikuje komendy na topicach
payment-commands
,training-commands
itp. - Każdy serwis nasłuchuje swojego topicu komend (np. Serwis Płatności –
payment-commands
). - Po wykonaniu, serwis publikuje zdarzenie na swoim topicu zdarzeń (np.
payment-events
), na które czeka orkiestrator.
Zobaczmy fragment kodu obrazujący prosty orkiestrator Sagi dla naszego procesu zapisu na szkolenie oraz obsługę jednej z komend:
// Orkiestrator Saga – koordynator procesu zapisu na szkolenie @Service public class EnrollmentSagaOrchestrator { @Autowired private KafkaTemplate<String, Object> kafkaTemplate; // Metoda rozpoczynająca Sagę zapisu kursanta public void startEnrollmentSaga(Long enrollmentId, Long studentId, Long trainingId) { // 1. Wyślij komendę do serwisu płatności, aby pobrał opłatę ProcessPaymentCommand cmd = new ProcessPaymentCommand(enrollmentId, studentId, trainingId); kafkaTemplate.send("payment-commands", cmd); // (Zakładamy, że dalsze kroki będą wywoływane w metodach obsługi odpowiedzi poniżej) } // 2. Nasłuchiwanie wyniku płatności @KafkaListener(topics = "payment-events", groupId = "saga-orchestrator") public void onPaymentEvent(PaymentEvent event) { if (event instanceof PaymentCompletedEvent) { PaymentCompletedEvent pay = (PaymentCompletedEvent) event; // Płatność ok -> wysyłamy komendę rezerwacji miejsca ReserveSeatCommand cmd = new ReserveSeatCommand(pay.getEnrollmentId(), pay.getTrainingId()); kafkaTemplate.send("training-commands", cmd); // (Dalsze kroki w kolejnym listenerze) } else if (event instanceof PaymentFailedEvent) { // Płatność nie przeszła -> kończymy Sagę kompensując (anulacja rejestracji) CancelEnrollmentCommand cancelCmd = new CancelEnrollmentCommand(event.getEnrollmentId()); kafkaTemplate.send("registration-commands", cancelCmd); } } // 3. Nasłuchiwanie wyniku rezerwacji miejsca @KafkaListener(topics = "training-events", groupId = "saga-orchestrator") public void onSeatEvent(SeatEvent event) { if (event instanceof SeatReservedEvent) { // Miejsce zarezerwowane -> potwierdź rejestrację ConfirmEnrollmentCommand confirmCmd = new ConfirmEnrollmentCommand(event.getEnrollmentId()); kafkaTemplate.send("registration-commands", confirmCmd); } else if (event instanceof SeatReservationFailedEvent) { // Brak miejsca -> zleć kompensację płatności (zwrot) RefundPaymentCommand refundCmd = new RefundPaymentCommand(event.getEnrollmentId()); kafkaTemplate.send("payment-commands", refundCmd); CancelEnrollmentCommand cancelCmd = new CancelEnrollmentCommand(event.getEnrollmentId()); kafkaTemplate.send("registration-commands", cancelCmd); } } }
// Serwis Płatności – obsługa komendy od orkiestratora @Service public class PaymentService { @Autowired private KafkaTemplate<String, Object> kafkaTemplate; @KafkaListener(topics = "payment-commands", groupId = "payment-service") public void onPaymentCommand(ProcessPaymentCommand cmd) { boolean paid = paymentProcessor.processPayment(cmd.getStudentId(), cmd.getTrainingId()); if (paid) { // odsyłamy zdarzenie z wynikiem pozytywnym kafkaTemplate.send("payment-events", new PaymentCompletedEvent(cmd.getEnrollmentId(), cmd.getTrainingId())); } else { // odsyłamy zdarzenie z wynikiem negatywnym kafkaTemplate.send("payment-events", new PaymentFailedEvent(cmd.getEnrollmentId(), cmd.getTrainingId())); } } }
Powyższy kod pokazuje uproszczony schemat działania Saga z orkiestratorem. EnrollmentSagaOrchestrator
to serwis, który inicjuje Sagę i reaguje na zdarzenia z poszczególnych kroków. Po rozpoczęciu (startEnrollmentSaga
) wysyła komendę ProcessPaymentCommand
na topic payment-commands
. Serwis Płatności nasłuchuje ten topic i wykonuje logikę płatności, po czym publikuje odpowiedni event (PaymentCompletedEvent
lub PaymentFailedEvent
na payment-events
). Orkiestrator (nasłuchujący payment-events
) odbiera wynik i w metodzie onPaymentEvent
decyduje co dalej: przy sukcesie płatności wysyła komendę rezerwacji miejsca (ReserveSeatCommand
na training-commands
), a przy porażce – od razu rozpoczyna kompensację, zlecając anulowanie rejestracji (CancelEnrollmentCommand
na registration-commands
). Analogicznie, wynik rezerwacji miejsca (SeatReserved
lub SeatReservationFailed
na training-events
) jest obsługiwany w onSeatEvent
. Po pozytywnej rezerwacji, orkiestrator zleca potwierdzenie rejestracji (ConfirmEnrollmentCommand
), a po negatywnej – uruchamia kompensacje: zwrot płatności (RefundPaymentCommand
) i anulację rejestracji.
Zauważmy, że w obu podejściach (choreografia i orkiestracja) posługujemy się Kafką do przesyłania zdarzeń/komend, jednak w modelu orkiestracji to orkiestrator wysyła komendy i interpretuje zdarzenia, podczas gdy w choreografii serwisy komunikują się bardziej bezpośrednio przez zdarzenia domenowe.
Warto wspomnieć, że istnieją gotowe frameworki ułatwiające implementację Saga w podejściu orkiestracji – np. orkiestrator oparty o state machine (maszynę stanów) lub dedykowane narzędzia jak Camunda, Temporal. W naszym przykładzie pokazaliśmy jednak, że można zrealizować Sagę samodzielnie, używając po prostu mechanizmu kolejek (Kafka) i trochę kodu koordynującego.
Saga – podsumowanie
Wzorzec Saga stał się fundamentalnym elementem projektowania spójności danych w architekturach mikroserwisowych. Pozwala pogodzić autonomię serwisów (własne bazy danych, brak silnych zależności) z potrzebą realizacji złożonych procesów biznesowych, które wymagają wielu kroków w różnych usługach.
Saga zastępuje tradycyjne transakcje rozproszone (jak 2PC) lżejszym mechanizmem opartym na zdarzeniach i kompensacjach, lepiej dopasowanym do środowisk rozproszonych i skalowalnych.
Omówiliśmy dwa podejścia do implementacji Sag:
- Choreografia – prosta w implementacji i rozszerzaniu, oparta na luźno powiązanych zdarzeniach, choć trudniejsza w analizie gdy proces mocno się rozrasta.
- Orkiestracja – z centralnym koordynatorem, zapewnia lepszą obserwowalność i kontrolę nad przepływem (szczególnie przy skomplikowanych scenariuszach i błędach), kosztem dodatkowego komponentu i silniejszego powiązania logiki.
W wielu realnych systemach wykorzystuje się hybrydę tych podejść – np. główny proces realizowany jest przez orkiestratora, ale poboczne działania (jak wysłanie e-maila) mogą być wpinane zdarzeniami w stylu choreografii. Niezależnie od wyboru stylu, kluczowe jest zrozumienie, że Saga to znacznie więcej niż kod – to pewien wzorzec myślenia o transakcjach rozproszonych. Projektując mikroserwisy, warto od początku uwzględnić mechanizmy umożliwiające implementację Sag (np. integrację z brokerem zdarzeń jak Kafka, przygotowanie operacji kompensacyjnych). Dzięki temu unikniemy pokusy niewłaściwych rozwiązań (jak nadmierne łączenie serwisów wspólną bazą czy ryzykowne próby użycia 2PC w środowisku rozproszonym).
Mam nadzieję, że ten materiał pokazał Ci praktycznie, jak Saga działa krok po kroku – od teorii do kodu. Teraz wiesz, że gdy następnym razem staniesz przed zadaniem zapewnienia spójności danych wśród wielu mikroserwisów, wzorzec Saga będzie potężnym narzędziem w Twoim repertuarze, pozwalającym zachować balans między niezależnością usług a niepodzielnością procesów biznesowych.
Powodzenia w implementacji Sag!
20+ BONUSOWYCH materiałów z programowania
e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!