Java

Saga, java, wzorzec projektowy

Wzorzec Saga w architekturze mikroserwisów – Spring Boot i Apache Kafka w praktyce

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.

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?

Saga vs 2fa

Ź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:

  1. Rejestracja kursanta – Serwis Rejestracji otrzymuje żądanie zapisu na szkolenie. Tworzy w swojej bazie wpis rejestracji ze statusem np. PENDING (oczekujące) i publikuje zdarzenie EnrollmentCreated (rejestracja utworzona).
  2. Płatność – Serwis Płatności odbiera zdarzenie EnrollmentCreated (np. z topicu Kafki enrollment-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 zdarzenie PaymentCompleted (płatność zakończona). Jeśli się nie powiedzie – publikuje zdarzenie PaymentFailed (płatność nieudana).
  3. Reakcja na płatność – Zdarzenie PaymentCompleted trafia do zainteresowanych serwisów. Serwis Rejestracji może je odebrać i zaktualizować status rejestracji na PAID (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 zdarzenie SeatReserved (miejsce zarezerwowane) albo SeatReservationFailed (brak miejsca).
  4. Finalizacja lub kompensacja – Załóżmy, że miejsce udało się zarezerwować – SeatReserved zostaje odebrane przez Serwis Rejestracji, który zmienia status rejestracji na CONFIRMED (potwierdzone) i np. emituje zdarzenie EnrollmentCompleted (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ń opublikuje SeatReservationFailed (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 zdarzenie PaymentRefunded). 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:

  1. 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.
  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, powiedzmy payment-commands, na którym słucha Serwis Płatności). Następnie orkiestrator czeka na wynik.
  3. Płatność (wynik) – Serwis Płatności otrzymuje komendę, wykonuje płatność i odsyła odpowiedź – np. publikuje zdarzenie PaymentCompleted lub PaymentFailed 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.
  4. 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 lub SeatReservationFailed.
  5. 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 zdarzenie EnrollmentCompleted (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) i CancelEnrollment 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!

No comments
Share:
spring data

Spring Data, JPA – przewodnik dla Junior Java Developera

Spring Data to potężne narzędzie w ekosystemie Spring, które znacznie upraszcza interakcje z bazami danych w aplikacjach Java. Dla początkujących programistów, którzy dopiero uczą się podstaw programowania, ten wpis pomoże zrozumieć, jak korzystać ze Spring Data do zarządzania danymi w swoich aplikacjach. W niniejszym przewodniku omówimy podstawowe pojęcia, konfigurację projektu, tworzenie encji, repozytoriów, podstawowe operacje CRUD oraz zapytania niestandardowe.

Z tego materiału dowiesz się:

  • Co to jest Spring Data?
  • Jak skonfigurować projekt z użyciem Spring Data?
  • Jak tworzyć encje w Spring Data?
  • Jak definiować repozytoria w Spring Data?
  • Jak wykonywać podstawowe operacje CRUD?
  • Jak korzystać z zapytań niestandardowych w Spring Data?

Co to jest Spring Data?

Spring Data to część większego ekosystemu Spring, która specjalizuje się w upraszczaniu interakcji z bazami danych. Dostarcza uniwersalny interfejs do pracy z różnymi typami baz danych, zarówno SQL, jak i NoSQL. Dzięki Spring Data możesz skupić się na logice biznesowej swojej aplikacji, zamiast tracić czas na pisanie skomplikowanych zapytań SQL.

Przykład Zastosowania

Wyobraź sobie aplikację zarządzającą biblioteką. Spring Data pozwala na łatwe tworzenie, czytanie, aktualizowanie i usuwanie rekordów książek w bazie danych, niezależnie od tego, czy używasz MySQL, MongoDB, czy innej bazy danych.

Podstawowe Pojęcia

Repository

Repository to interfejs, który definiuje metody do interakcji z bazą danych. W Spring Data, repository jest głównym elementem odpowiedzialnym za wykonywanie operacji CRUD (Create, Read, Update, Delete).

Entity

Entity to klasa w Javie, która jest mapowana na tabelę w bazie danych. Każda instancja tej klasy reprezentuje jeden wiersz w tabeli. Encje są zazwyczaj annotowane przy użyciu @Entity, a pola klasy są mapowane na kolumny tabeli.

CRUD

CRUD to akronim oznaczający podstawowe operacje wykonywane na danych: Create (tworzenie), Read (czytanie), Update (aktualizacja) i Delete (usuwanie). Dzięki repozytoriom Spring Data, te operacje są proste do zaimplementowania.

➡ ZOBACZ 👉: CRUD, Create | Read | Update | Delete [CRUD] 🛠️📖✍️❌

Konfiguracja Projektu

Zacznijmy od stworzenia nowego projektu Spring Boot z zależnościami do Spring Data JPA oraz bazy danych H2.

Przykładowy plik pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

➡ ZOBACZ 👉: Spring Boot i wstrzykiwanie zależności – szybkie wprowadzenie

Tworzenie Encji

Stwórzmy klasę User, która będzie mapowana na tabelę w bazie danych. Każda encja powinna być annotowana przy użyciu @Entity. Identyfikator główny (id) jest annotowany jako @Id.

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.util.UUID;

@Entity
@Table(name = "users")
public class User {

	@Id
	private UUID id;

	private String name;
	private String email;

	public User() {
		this.id = UUID.randomUUID();
	}

    // Gettery i settery
}

➡ ZOBACZ 👉: SQL

Tworzenie Repozytorium

Następnie, stworzymy interfejs UserRepository, który będzie rozszerzał JpaRepository. Dzięki temu zyskamy dostęp do zestawu gotowych metod CRUD.

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

Podstawowe Operacje CRUD

Dodawanie użytkownika

Dodawanie nowego użytkownika do bazy danych jest proste dzięki metodzie save.

User user = new User();
user.setName("Jan Kowalski");
user.setEmail("jan.kowalski@example.com");
userRepository.save(user);

Pobieranie użytkownika

Aby pobrać użytkownika z bazy danych na podstawie jego identyfikatora, używamy metody findById.

User user = userRepository.findById(id).orElse(null);

Aktualizacja użytkownika

Aktualizacja istniejącego użytkownika jest równie prosta jak jego dodanie. Wystarczy zmienić potrzebne pola i ponownie zapisać obiekt.

User user = userRepository.findById(id).orElse(null);
if (user != null) {
    user.setName("Jan Nowak");
    userRepository.save(user);
}

Usuwanie użytkownika

Usuwanie użytkownika z bazy danych odbywa się za pomocą metody deleteById.

userRepository.deleteById(id);

Korzystanie z zapytań niestandardowych

Spring Data pozwala na definiowanie własnych metod zapytań w repozytoriach. Możemy korzystać z konwencji nazewniczych lub używać annotacji @Query do definiowania zapytań.

Przykład: Znajdowanie użytkowników po imieniu

import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);
}

 

List<User> users = userRepository.findByName("Tomek");

Przykład: Zapytanie niestandardowe z @Query

import org.springframework.data.jpa.repository.Query;

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.email = ?1")
    User findByEmail(String email);
}

 

User user = userRepository.findByEmail("jan.kowalski@example.com");

Korzyści ze Stosowania Spring Data

Automatyzacja

Spring Data automatyzuje wiele rutynowych zadań związanych z dostępem do danych, co pozwala programistom skupić się na logice biznesowej.

Abstrakcja Bazy Danych

Umożliwia pracę z różnymi typami baz danych (SQL, NoSQL) przy użyciu spójnego interfejsu.

Integracja z Ekosystemem Spring

Łatwa integracja z innymi komponentami Spring, takimi jak Spring Boot, Spring Security, czy Spring Batch.


Podsumowanie

Spring Data jest niezwykle przydatnym narzędziem dla programistów Java, które znacznie upraszcza pracę z bazami danych. Mam nadzieję, że ten przewodnik pomógł Ci zrozumieć podstawowe pojęcia i operacje związane ze Spring Data. Teraz możesz śmiało eksperymentować i rozwijać swoje umiejętności!

Jeśli masz pytania lub chcesz podzielić się swoimi doświadczeniami, zostaw komentarz poniżej!

No comments
Share:
springboot

Spring Boot, Spring REST API

Jak za pomocą Javy, Mavena, SpringBoot’a stworzyć proste API REST-owe?

Z tego materiału dowiesz się:

  • Jak utworzyć nowy projekt w IntelliJ IDEA za pomocą Spring Initializer?
  • Jak skonfigurować projekt Maven ze Spring Boot?
  • Jakie są podstawowe zależności potrzebne do stworzenia API RESTowego?
  • Jak zorganizować strukturę katalogów w projekcie Spring Boot?
  • Jak utworzyć prostą klasę kontrolera REST w Spring Boot?
  • Jak uruchomić aplikację Spring Boot i przetestować jej działanie?
  • Jak mapować metody kontrolera na odpowiednie żądania HTTP?
  • Jakie są zalety wersjonowania API i jak je zaimplementować?
  • Jak sprawdzić poprawność działania aplikacji za pomocą przeglądarki?

Tworzenie Nowego Projektu

Rozpoczynamy od utworzenia nowego projektu w IntelliJ IDEA. Możesz to zrobić przez File -> New -> Project. Jeśli masz pełną wersję, skorzystaj ze Spring Initializer. Alternatywnie, użyj strony StartSpring.io.

➡ ZOBACZ👉: IDE Zintegrowane środowisko programistyczne | Kurs Java

Konfiguracja Projektu

  1. Wybór lokalizacji: Wybierz, gdzie chcesz utworzyć nowy projekt.
  2. Nazwa projektu i pakietu: Określ nazwę projektu oraz główny pakiet.
  3. Wersja Javy: Pracujemy na wersji Java 21, z pakowaniem jar.
  4. Wybór zależności: Dodaj Spring Web – na początek wystarczy.

Struktura Projektu

Projekt utworzony! Zajrzyjmy do pliku pom.xml w głównym katalogu – to tutaj znajdziesz wszystkie zależności. Pamiętaj, że pierwsze uruchomienie może chwilę potrwać, gdyż muszą się pobrać wszystkie potrzebne zasoby.

➡ ZOBACZ👉: Spring Boot i wstrzykiwanie zależności – szybkie wprowadzenie

Katalogi

  • src/main: Zawiera źródła aplikacji.
  • src/test: Znajdziesz tu testy, które warto uruchomić, aby sprawdzić, czy wszystko działa poprawnie.

Tworzenie Kontrolera REST

Stwórz nową klasę, np. StormResources, i dodaj metodę zwracającą aktualny czas:

@RestController
@RequestMapping("/api/v1")
public class StormResources {
    @GetMapping("/test")
    public String testReturn() {
        return "Current time: " + System.currentTimeMillis();
    }
}

Adnotacja @RestController informuje Spring, że jest to kontroler RESTowy. Metoda testReturn zwraca ciąg znaków z aktualnym czasem.

Uruchomienie Aplikacji

Uruchom aplikację, a następnie sprawdź w przeglądarce adres http://localhost:8080/api/v1/test. Jeśli zobaczysz zmieniające się liczby, to znak, że wszystko działa poprawnie!

➡ ZOBACZ👉: Spring Boot i wstrzykiwanie zależności – szybkie wprowadzenie

Mapowanie metod HTTP

Skorzystaj z adnotacji takich jak @GetMapping, aby przypisywać metody kontrolera do konkretnych żądań HTTP, co pozwala na obsługę różnych typów zapytań (GET, POST, itp.).

Wersjonowanie API

Korzystamy z wersjonowania API, co ułatwia zarządzanie zmianami w przyszłości, i dodaję wersję do ścieżki URL swojego API przy użyciu adnotacji @RequestMapping.

Sprawdzenie poprawności działania aplikacji za pomocą przeglądarki

Uruchamiamy aplikację i testujemy jej działanie poprzez wpisanie odpowiedniego adresu URL w przeglądarce, co pozwala na szybką weryfikację, czy aplikacja działa poprawnie.

Podsumowanie

Gratulacje, w ten sposób właśnie powstaje API RESTowe! Jeśli chcesz dalej rozwijać swoje umiejętności w Javie, zapraszam na stronę

👉: Kurs Java

gdzie znajdziesz więcej materiałów.

No comments
Share:

Java 10 dni – Lekcja 8, Kontrolowanie przepływu

 

 

No comments
Share:

Java 10 dni – Lekcja 7, Komunikacja z użytkownikiem

 

No comments
Share:

Java 10 dni – Lekcja 6, Metody i wyrażenia

 

No comments
Share:

Java 10 dni – Lekcja 5, Zmienne i typy danych

 

No comments
Share:

Java 10 dni – Lekcja 4, Jak wykonywany jest kod naszej aplikacji? 🚂🚊🚅🚝

 

https://www.youtube.com/watch?v=aAEJn3QcgQU

No comments
Share:

Java 10 dni – Lekcja 3, Narzędzia programistyczne, które ułatwiają nam pracę

  • Java 10 dni – Lekcja 3, Narzędzia programistyczne, które ułatwiają nam pracę
  • Pełny materiał dostępny na:

 

No comments
Share:

Java 10 dni – Lekcja 2, Twój pierwszy program – aplikacja „Hello World”

 

No comments
Share: