Od monolitu do mikroserwisów – jaki masz cel?
Na początku warto doprecyzować, po co w ogóle chcesz budować architekturę mikroserwisów w Javie z użyciem Spring Boot, Apache Kafka i Kubernetes. Czy kluczowe jest skalowanie, niezależne wdrożenia, czy raczej rozdzielenie odpowiedzialności w zespole? Od tej odpowiedzi zależy prawie każda kolejna decyzja architektoniczna.
Kiedy mikroserwisy w Javie mają sens, a kiedy nie?
Mikroserwisy w ekosystemie Java sprawdzają się szczególnie wtedy, gdy:
- masz złożoną domenę podzieloną na wyraźne obszary biznesowe (np. zamówienia, płatności, katalog, fakturowanie),
- zespoły są większe i możesz przypisać 1–2 serwisy do jednego zespołu,
- wymagasz niezależnych wdrożeń – np. musisz często zmieniać logikę płatności, nie dotykając reszty systemu,
- skalowanie wymaga różnej charakterystyki – np. serwis katalogu jest mocno czytany, a serwis płatności głównie zapisuje i waliduje.
Jeśli natomiast:
- zespół liczy 2–3 osoby,
- domena nie jest jeszcze dobrze zrozumiana,
- zmiany w jednej części prawie zawsze wymagają zmian w innych,
to rozbicie na mikroserwisy może przynieść więcej szkody niż pożytku. Częściej korzystniejsze jest wtedy rozpoczęcie od modularnego monolitu z jasnymi granicami domen, a dopiero później stopniowe wydzielanie mikroserwisów.
Jak wygląda to u ciebie? Budujesz wszystko od zera, czy migrujesz istniejący monolit? Jeśli masz już system w produkcji, zadaj sobie pytanie: gdzie dziś najbardziej cierpisz – na wydajności, na częstych konfliktach merge w jednym repozytorium, czy na braku możliwości niezależnego wdrażania?
Diagnoza obecnego systemu – gdzie naprawdę boli?
Dobrym ćwiczeniem jest spisanie na kartce (dosłownie) 3–5 najboleśniejszych problemów, które chcesz rozwiązać mikroserwisami. Przykłady:
- „Przy każdej zmianie w logice rabatów muszę wdrażać całą aplikację, co trwa długo i jest ryzykowne.”
- „W godzinach szczytu baza dostaje w kość od raportów, które wyhamowują operacje na zamówieniach.”
- „Jeden błąd w module faktur potrafi położyć cały system.”
Jeśli większość problemów dotyczy:
- skalowania (wąskie gardła wydajności),
- stabilności (jeden moduł kładzie wszystko),
- niezależności wdrożeń (ciężko wprowadzać zmiany w jednym obszarze),
wtedy mikroserwisy oparte na Spring Boot, komunikacji asynchronicznej i orkiestracji na Kubernetes mają znacznie większy sens. Jeśli natomiast główny ból to np. jakość kodu lub brak testów, migracja do mikroserwisów może jedynie pomnożyć problemy.
Granice systemu i główne domeny biznesowe
Zanim powstanie pierwszy serwis w Spring Boot, przyda się odpowiedzieć na pytanie: jakie trzy główne procesy biznesowe są dla ciebie kluczowe? Przykładowo w systemie e-commerce mogą to być:
- przyjmowanie zamówień,
- obsługa płatności,
- wystawianie faktur.
Do tego dochodzą często procesy pomocnicze – zarządzanie katalogiem produktów, użytkownikami, raportowanie. Każdy z tych obszarów może być kandydatem na osobny bounded context, a potem na mikroserwis. Na tym etapie nie myśl jeszcze o klasach, paczkach czy tabelach – skup się na słownictwie biznesowym, głównych przepływach i na tym, kto (jaka rola) odpowiada za dany fragment.
Dobre pytanie kontrolne: czy umiesz na jednej kartce narysować mapę domen – 3–7 prostokątów opisanych nazwami obszarów biznesowych i strzałkami pokazującymi najważniejsze przepływy? Jeśli nie, trudno będzie zbudować spójną architekturę mikroserwisów w Javie, nawet z najlepszymi narzędziami.
Modelowanie domeny i granic serwisów (DDD w wersji „dla programisty”)
Domain-Driven Design często brzmi jak teoria dla architektów, ale w kontekście Spring Boot, Kafka i Kubernetes liczy się bardzo praktyczne podejście: jak ten język domenowy przełożyć na konkretne mikroserwisy, paczki i kontrakty API.
Ubiquitous language i bounded context w praktyce Javy
Ubiquitous language oznacza, że nazwy klas, metod, zdarzeń i REST endpointów odzwierciedlają język biznesu. Jeśli w organizacji mówi się o „zamówieniu”, „płatności”, „fakturze”, nie ma sensu w kodzie wprowadzać „OrderRecord”, „ChargeDTO” i „BillEntity”, chyba że stoi za tym jasna różnica domenowa.
Bounded context z kolei to granica, w której dany język jest spójny. Przykład:
- W kontekście „Zamówienia” pojęcie „Order” oznacza proces złożenia i realizacji zamówienia.
- W kontekście „Fakturowanie” „Order” może być widziany już tylko jako źródło danych do wystawienia dokumentu.
W praktyce Javy:
- każdy bounded context staje się osobnym mikroserwisem (albo na początku osobnym modułem w monolicie),
- w kodzie porządkuj paczki, np. com.example.order, com.example.payment, com.example.invoice,
- unika się współdzielenia encji JPA między kontekstami; zamiast tego stosuje się własne modele danych i integracje przez REST lub zdarzenia w Kafka.
Jak dzielić domenę: funkcjonalnie, wokół zdarzeń czy wokół danych?
Masz przynajmniej trzy osie podziału:
- Funkcjonalna – serwisy odpowiadają za wyraźne funkcje biznesowe (Zamówienia, Płatności, Katalog).
- Wokół zdarzeń – jeden serwis skupia się na emitowaniu zdarzeń, inne je subskrybują i reagują (np. serwis „Order” emituje „OrderCreated”, a serwisy „Payment” i „Invoice” reagują).
- Wokół danych – serwisy są budowane wokół określonych agregatów danych (Klient, Produkt, Zamówienie), ale tu łatwo wpaść w pułapkę „serwis = tabela w bazie”.
Najczęstszy błąd: kopiowanie struktur bazy danych jako granic mikroserwisów. Jeśli masz tabelę ORDERS, nie oznacza to automatycznie, że potrzebujesz osobnego mikroserwisu „Orders” z jednym CRUD-em. Mikroserwis powinien reprezentować proces lub odpowiedzialność, nie tylko CRUD nad tabelą.
Zadaj sobie pytanie: czy dany serwis można opisać zdaniem „Serwis X odpowiada za …” i zakończyć je czasownikiem, a nie tylko „… za dane o …”? Jeśli jedynym uzasadnieniem jest „bo tam są te dane”, lepiej jeszcze raz przemyśleć podział.
Wewnętrzna architektura mikroserwisu – prosto, ale z głową
Nawet jeśli budujesz mikroserwisy, w środku dalej potrzebujesz porządku. Dobrym punktem wyjścia jest architektura hexagonalna (ports & adapters) lub clean architecture. W praktyce w Spring Boot możesz to przekuć na prosty, ale klarowny podział:
- warstwa API – kontrolery REST, kontrolery GraphQL, dostęp do messagingu (np. wejściowe kanały Kafki),
- warstwa aplikacyjna – use case’y, serwisy biznesowe, logika procesowa,
- warstwa domenowa – encje domenowe, agregaty, obiekty wartości, reguły biznesowe,
- warstwa infrastruktury – repozytoria JPA, adaptery do Kafki, integracje z zewnętrznymi API.
Taki podział ułatwia późniejsze testowanie (np. testy domenowe bez Springa) i utrzymanie, a także przeniesienie logicznego modułu z monolitu do osobnego mikroserwisu.
Czy umiesz narysować mapę domen w 10 minut?
Przydatne ćwiczenie: spróbuj narysować na kartce prostą mapę domeny z 3–7 kontekstami. Bez narzędzi UML, bez długich warsztatów. Jeśli nie jesteś w stanie, albo dostajesz kilkanaście „kontekstów” na start, to sygnał, że trzeba jeszcze popracować nad zrozumieniem domeny, zanim zaczniesz mnożyć mikroserwisy, klastry Kubernetesa i topiki w Kafka.

Projekt techniczny mikroserwisu w Spring Boot – fundamenty
Załóżmy, że mapa domeny jest już wstępnie narysowana. Kolejny krok to zaprojektowanie pojedynczego mikroserwisu w Spring Boot tak, aby był powtarzalnym „klockiem” i łatwo integrował się z Kafka i Kubernetes.
Minimalny szkielet mikroserwisu Spring Boot
Typowa struktura katalogów dla mikroserwisu „order-service” może wyglądać tak:
com.example.order.api– kontrolery REST, DTO, modele API,com.example.order.application– serwisy aplikacyjne, use case’y,com.example.order.domain– encje domenowe, logika biznesowa,com.example.order.infrastructure– repozytoria, konfiguracje Kafki, adaptery do zewnętrznych systemów.
Podstawowa klasa aplikacji Spring Boot to standardowy @SpringBootApplication. Kluczowe są natomiast zależności i konfiguracja. Do typowego mikroserwisu potrzebujesz co najmniej:
spring-boot-starter-web– dla REST,spring-boot-starter-actuator– dla metryk, health-checków,spring-boot-starter-data-jpa(lub innego mechanizmu dostępu do danych),spring-kafkalubspring-cloud-stream– dla integracji z Apache Kafka.
Konfigurację trzymaj w plikach application.yml (profil dev) i osobnych application-*.yml dla innych środowisk, a na K8s nadpisuj je przez ConfigMap/Secret i zmienne środowiskowe.
REST kontra komunikacja zdarzeniowa
Kluczowe pytanie: kiedy używać REST, a kiedy oprzeć się na zdarzeniach w Kafka? Prosta zasada:
- REST – do scenariuszy zapytań, synchronizacji, kiedy potrzebujesz natychmiastowej odpowiedzi („podaj szczegóły zamówienia”, „sprawdź stan płatności”).
- Kafka – do propagacji zdarzeń biznesowych, reakcji asynchronicznej, integracji luźno powiązanych serwisów („zamówienie utworzone”, „płatność zautoryzowana”, „faktura wystawiona”).
Czy da się wszystko zrobić REST-em? Technicznie tak, ale skończysz z kaskadą wywołań typu „OrderService → PaymentService → InvoiceService” i dużą podatnością na błędy po drodze. Eventy w Kafka pozwalają „rozluźnić” te powiązania: serwis zamówień publikuje OrderCreated, a serwisy płatności i faktur tylko nasłuchują.
Konfiguracja zewnętrzna i Spring Cloud
Mikroserwisy na Kubernetes powinny być jak najbardziej „dwupłciowe”: kod jest stały, a konfiguracja wstrzykiwana z zewnątrz. Do wyboru masz kilka podejść:
- Spring Cloud Config – centralne repozytorium konfiguracji (np. Git), z którego serwisy pobierają ustawienia,
- ConfigMap/Secret na Kubernetes – konfiguracja utrzymywana jako zasoby klastra,
- połączenie obu: Spring Cloud Config jako źródło, a na K8s tylko parametry środowiskowe (URL do Config Servera itd.).
Istotne jest wersjonowanie i możliwość odtworzenia konfiguracji dla danej wersji serwisu. Jeśli używasz Config Servera, trzymaj konfigurację w repozytorium Git obok kodu, ale w osobnym module. Jeśli wszystko konfigurujesz przez K8s, utrzymuj manifesty w tym samym monorepo co serwisy (np. osobny katalog k8s).
Zarządzanie zależnościami i wersjami
W świecie Spring Boot i Spring Cloud pomocny jest BOM (Bill of Materials). W pom.xml możesz importować BOM Spring Boot, co pozwala spójnie zarządzać wersjami zależności:
- unikasz ręcznego dobierania wersji Spring Kafka, Spring Cloud itd.,
- wszystkie serwisy w organizacji powinny używać tej samej wersji Spring Boot (np. 3.x), aby uprościć utrzymanie.
Staraj się nie mieć „zoo wersji” – jeden serwis na Boot 2.x, inny na 3.x, jeszcze inny na snapshotach. Aktualizacje rób falami, ale świadomie i z testami.
Przykład „Hello Domain Service”: prosty serwis zamówień
Wyobraź sobie prosty serwis „order-service”:
- REST endpoint
POST /orderstworzy nowe zamówienie, - zapisuje je w bazie (np. PostgreSQL przez JPA),
- publikuje zdarzenie OrderCreated do topiku Kafka
orders.
Od strony Spring Boot:
- w warstwie API – kontroler z metodą przyjmującą DTO zamówienia,
Warstwa aplikacyjna: gdzie kończy się REST, a zaczyna domena?
W przykładzie order-service kontroler REST nie powinien znać szczegółów logiki biznesowej ani sposobu publikacji zdarzeń w Kafka. Jego rola to przyjęcie żądania, walidacja podstawowa i delegacja do warstwy aplikacyjnej.
Przykładowy schemat przepływu wygląda tak:
OrderControllerprzyjmujeCreateOrderRequest,- mapuje je na komendę aplikacyjną lub prosty obiekt wejściowy,
- woła
OrderApplicationService.createOrder(...), - serwis aplikacyjny tworzy agregat domenowy
Order, - agregat generuje zdarzenie domenowe
OrderCreatedEvent, - warstwa aplikacyjna przekazuje zdarzenie do adaptera Kafki w warstwie infrastruktury.
Zapytaj siebie: czy aktualnie masz w kontrolerach logikę typu if (status == PAID) ..., wyliczanie rabatów, złożone walidacje? Jeśli tak, przenieś je do serwisów aplikacyjnych lub domeny. Kontroler REST ma „rozmawiać z HTTP”, nie z biznesem.
Zdarzenia domenowe wewnątrz mikroserwisu
Zanim wyślesz coś do Kafka, dobrze mieć spójny model zdarzeń w obrębie samego serwisu. Inaczej szybko skończysz z przypadkowymi JSON-ami tworzonymi ad hoc „pod konkretny przypadek”.
Najprostsze podejście:
- w domenie definiujesz zdarzenia jako klasy (np.
OrderCreated,OrderCancelled), - agregaty wystawiają je po wykonaniu operacji (np. metoda
create()zwraca agregat wraz z listą zdarzeń), - warstwa aplikacyjna zbiera te zdarzenia i przekazuje do event publishera,
- publisher mapuje zdarzenia domenowe na zdarzenia integracyjne (kontrakty API zdarzeniowego) i publikuje do Kafka.
Po co ten dodatkowy poziom? Zdarzenia domenowe mogą zmieniać się nieco częściej niż kontrakt integracyjny. Jeśli zapewnisz cienką warstwę mapowania, zyskujesz bufor na ewolucję bez natychmiastowego psucia integracji z innymi serwisami.
Na koniec warto zerknąć również na: Jak łączyć GraphQL, REST i eventy w chmurowym projekcie Java? — to dobre domknięcie tematu.
Zadaj sobie pytanie: czy format JSON, który wysyłasz do Kafka, to już „publiczny kontrakt”, czy tylko wewnętrzna struktura? Od odpowiedzi zależy, jak ostrożny musisz być przy zmianach.
Wersjonowanie API REST i kontraktów zdarzeniowych
W mikroserwisach w Javie zmiana jednego DTO może doprowadzić do lawiny problemów po stronie konsumentów. Jak tego nie powielać?
Dla REST najpopularniejsze podejścia to:
- wersjonowanie w URL –
/api/v1/orders,/api/v2/orders, - wersjonowanie w nagłówku – np.
Accept: application/vnd.example.orders.v1+json.
W praktyce, przy Spring Boot i klasycznych API B2B, wersjonowanie w ścieżce jest czytelniejsze i łatwiejsze do ogarnięcia w gatewayu. Dla zdarzeń w Kafka zwyczajowo wersjonuje się schemat (np. Avro Schema v1, v2) lub wprowadza nowy typ zdarzenia.
Prosta reguła: jeśli zmiana jest wstecznie kompatybilna (dodanie nowego pola nieobowiązkowego) – możesz zachować ten sam typ zdarzenia. Jeśli usuwasz/zmieniasz semantykę pola – rozważ nowy typ zdarzenia (np. OrderCreatedV2) albo nowy schemat i stopniową migrację konsumentów.
Sprawdź swoje aktualne API: czy masz jasno zaznaczoną wersję i plan wycofywania starych? Jeśli nie, przygotuj chociaż minimalny proces „deprecation”: oznaczenie w dokumentacji, czas życia, data usunięcia.
Testowanie mikroserwisów: od jednostkowych po kontraktowe
Im bardziej rozproszona architektura, tym szybciej zwykłe testy jednostkowe przestają wystarczać. Potrzebujesz kilku poziomów testów, ale bez paraliżowania zespołu.
Typowy zestaw dla mikroserwisu w Spring Boot:
- testy domenowe – czysta Java, bez Springa, testujące agregaty i reguły biznesowe,
- testy warstwy aplikacyjnej – z lekkim Spring Contextem (np.
@SpringBootTestzwebEnvironment = NONE), - testy kontraktowe API REST – np. Spring Cloud Contract lub zwykłe testy HTTP z mockiem bazy,
- testy integracji z Kafka –
@EmbeddedKafkalub testcontainers z prawdziwą Kafką w Dockerze, - testy end-to-end – dopiero na wspólnym środowisku (np. testowym namespace w Kubernetes).
Zastanów się: gdzie dzisiaj łapiesz najwięcej błędów – na produkcji, na testach systemowych, czy już w jednostkowych? Tam, gdzie najpóźniej pojawiają się błędy, tam zwykle brakuje odpowiedniego poziomu testów.
Idempotencja i powtarzalne przetwarzanie
Zdarzenia w Kafka mogą być dostarczone więcej niż raz (semantyka at-least-once). Jeśli Twój serwis przy każdej wiadomości tworzy nowy rekord w bazie lub zleca płatność, będziesz mieć problem.
Jak podejść do idempotencji?
- Każdemu zdarzeniu nadaj unikalny
eventId(np. UUID) lubbusinessKey(np.orderId+ typ operacji). - W konsumerze Kafka przed przetworzeniem sprawdzaj, czy operacja o takim
eventIdjuż została wykonana (osobna tabela, cache). - Stosuj upsert (insert or update) w bazie, jeśli to ma sens dla danej operacji.
- Przemyśl, które operacje są naprawdę nieodwracalne (np. zewnętrzna płatność), a które mogą być zrealizowane wielokrotnie bez konsekwencji (np. wysłanie e-maila informacyjnego z flagą „już wysłano”).
Zadaj sobie pytanie przy każdym konsumerze: co się stanie, jeśli to samo zdarzenie przyjdzie dwa, trzy razy? Jeśli odpowiedź brzmi „będzie bałagan”, dorzuć warstwę idempotencji.
Komunikacja asynchroniczna z Apache Kafka – projektowanie przepływu zdarzeń
Event-driven vs event-sourced – o czym w ogóle mówisz?
W rozmowach o Kafka często mieszają się dwa podejścia:
- Event-driven architecture (EDA) – serwisy reagują na zdarzenia, ale stan trzymają klasycznie w swoich bazach; Kafka jest kanałem komunikacji,
- Event sourcing – stan systemu jest odtwarzany ze strumienia zdarzeń, baza to głównie snapshoty i projekcje.
Większość projektów enterprise w Javie z Spring Boot używa EDA, nie pełnego event sourcingu. Czyli: klasyczna baza (np. PostgreSQL) + Kafka do komunikacji. Jeśli dopiero budujesz pierwszą architekturę mikroserwisową, zacznij właśnie od EDA, a nie od pełnego event sourcingu – unikniesz nadmiarowej złożoności.
Zapytaj siebie: czy naprawdę potrzebujesz odtwarzania całej historii obiektu z eventów, czy raczej chodzi o luźne powiązanie serwisów? Jeśli to drugie, event sourcing może być „armatą na muchę”.
Jak projektować topiki Kafka pod mikroserwisy?
Struktura topików to fundament. Źle nazwane i źle podzielone topiki szybko wprowadzą chaos.
Kilka praktycznych wskazówek:
- Buduj topiki wokół kontekstów i strumieni zdarzeń, np.
order.events,payment.events, a nie wokół konsumentów. - Wewnątrz topiku przechowuj różne typy zdarzeń (np.
OrderCreated,OrderCancelled) wyróżniane polemtypelub nagłówkiem. - Nie twórz osobnego topiku dla każdego eventu w małych domenach – skończysz z dziesiątkami topików trudno zarządzalnych.
- Używaj klucza wiadomości (record key) jako identyfikatora agregatu, np.
orderId, dzięki czemu wszystkie zdarzenia danego zamówienia trafią do tej samej partycji.
Dopytaj siebie: kto jest właścicielem danego topiku? Dobrą praktyką jest zasada: właścicielem jest zespół, który publikuje wydarzenia, a konsumenci są jego „klientami”. To pomaga w zarządzaniu zmianami i odpowiedzialnością.
Projektowanie schematów zdarzeń: JSON, Avro, Protobuf?
Java + Spring Boot często oznacza JSON – bo łatwy do odczytu i debugowania. Jednak przy większej skali wchodzi temat kontraktów i ewolucji schematów.
Masz trzy popularne warianty:
- JSON + walidacja po stronie aplikacji – proste na start, wymaga dyscypliny przy zmianach.
- Avro + Schema Registry – zdefiniowane schematy, walidacja w czasie publikacji/odczytu, kontrola ewolucji (backward/forward compatible).
- Protobuf – dobre przy wielu językach (Java, Go, Node), mniejszy payload, wymaga generatorów.
Jeśli dopiero budujesz pierwszą architekturę z Kafka, zacznij od JSON-a, ale z jasnym kontraktem: repozytorium z definicjami zdarzeń, opis pól, reguły zmian. Jeśli w organizacji rośnie liczba integracji, rozważ wejście w Schema Registry (Confluent lub kompatybilny) i Avro.
Zadaj sobie pytanie: ilu masz zewnętrznych konsumentów zdarzeń, a ilu „wewnątrz zespołu”? Im więcej niezależnych konsumentów, tym bardziej opłaca się zainwestować w formalne schematy.
Projekt przepływu zdarzeń: proces biznesowy na Kafka
Weź prosty proces: „utworzenie zamówienia i obsługa płatności”. Jak to rozrysować na zdarzeniach?
Przykładowy scenariusz:
order-servicepublikujeOrderCreatednaorder.events.payment-servicesubskrybujeOrderCreated, rezerwuje środki, publikujePaymentAuthorizednapayment.events.order-servicesubskrybujePaymentAuthorized, zmienia status zamówienia na „PAID” i publikujeOrderPaid.invoice-servicesubskrybujeOrderPaid, wystawia fakturę itd.
Teraz pytanie kontrolne: czy gdziekolwiek w tym procesie masz silną potrzebę zatrzymania całego procesu i zwrotu błędu użytkownikowi w czasie rzeczywistym? Jeśli tak, to ten krok powinien być raczej REST-owy (np. inicjacja płatności przez klienta), a dalsze kroki – asynchroniczne.
Dobrym zwyczajem jest posiadanie prostych widoków odczytowych (np. order-status-service), które łączą dane z kilku strumieni zdarzeń i udostępniają je przez REST – zamiast robić „REST hop” między pięcioma mikroserwisami tylko po to, by złożyć status.
Obsługa błędów i retry w asynchronicznym świecie
W Spring Boot + Kafka typowe problemy zaczynają się, gdy konsumer rzuca wyjątkiem. Co wtedy? Zależnie od konfiguracji, masz kilka opcji:
- ponowne próby (retry) w ramach tego samego konsumenta,
- przeniesienie wiadomości do dead-letter topic (DLT),
- kombinacja: kilka prób, potem DLT.
Praktyczne podejście:
- Ustal maksymalną liczbę prób i odstępy między nimi (może być wykładniczy backoff).
- Dla błędów „techniczych” (brak połączenia z bazą, chwilowy błąd sieci) retry ma sens.
- Dla błędów „biznesowych” (np. niepoprawne dane, brak klienta w systemie) szybciej kieruj wiadomość do DLT.
- Monitoruj DLT osobno (np. osobny dashboard w Grafanie) i miej proces „sprzątania” z udziałem człowieka lub narzędzia naprawczego.
Sprawdź swój kod konsumerów: czy wszystkie wyjątki są łapane i odpowiednio klasyfikowane? Jeśli wszędzie rzucasz RuntimeException, to masz tylko jeden „kanał” błędów i trudno nad tym zapanować.

Integracja Spring Boot z Kafka – praktyczne schematy
Producent zdarzeń w Spring Kafka: gdzie umieścić logikę?
Producent (publisher) to kluczowy element, bo od jego stabilności zależy spójność stanu między bazą a zdarzeniami. Podstawowy dylemat: czy najpierw zapisać w bazie, czy najpierw opublikować event?
Typowy, bezpieczniejszy schemat przy użyciu wzorca outbox wygląda tak:
- Transakcja w bazie: zapis zamówienia + zapis wpisu do tabeli outbox (z JSON-em zdarzenia i statusem „NEW”).
- Osobny proces (Spring Scheduler, Debezium, lub osobny „outbox dispatcher”) czyta tabelę outbox i publikuje zdarzenia do Kafka.
- Po udanej publikacji aktualizuje status wpisu (np. „SENT”).
Transakcje, dokładność „co najmniej raz” i outbox w praktyce
Masz już schemat z tabelą outbox. Teraz pytanie: jak zapewnić spójność, kiedy coś pójdzie nie tak – Kafka jest chwilowo niedostępna, broker zwróci timeout, scheduler się wysypie?
Kilka praktycznych zasad:
- Outbox w tej samej bazie co dane domenowe – wtedy jedna transakcja obejmuje i agregat (np. zamówienie), i wpis outbox.
- Retry po stronie dispatchera – dispatcher powinien próbować wysłać zdarzenie tyle razy, ile potrzeba, aż do sukcesu; outbox działa tu jak bufor.
- Brak „rollbacku” zdarzeń – jeśli coś zapisałeś w bazie i wystawiłeś event, nie próbuj „wymazywać historii”; użyj kolejnych zdarzeń kompenacyjnych (np.
PaymentFailed).
Zadaj sobie pytanie: co się stanie, jeśli dispatcher wyśle to samo zdarzenie dwa razy? Jeśli konsumer jest idempotentny, pozostaje tylko kwestia monitoringu i ewentualnej analizy logów, a nie długa noc na naprawianiu produkcji.
Typowy kod dispatchera w Spring Boot to zwykle:
Jeśli chcesz ugruntować fundamenty samej Javy i ekosystemu, sensownie jest czasem na chwilę wrócić do podstaw i poukładać je systemowo – tu przydają się materiały typu więcej o programowanie, zwłaszcza jeśli później chcesz wskoczyć w bardziej rozproszone architektury.
- zadanie schedulera lub listener CDC (np. Debezium),
- odczyt kilku/kilkudziesięciu rekordów z outboxa,
- wysłanie przez
KafkaTemplatezkey+value, - aktualizacja statusu w outboxie.
Jeśli próbujesz to „sprytnie” upchnąć w ramach jednego requestu HTTP (REST + Kafka w jednym kroku), zadaj sobie pytanie: czy naprawdę chcesz, aby chwilowy problem z brokerem zablokował odpowiedź API?
Consumer w Spring Kafka: separacja warstw i wzorce
Po stronie konsumenta często miesza się logika transportowa (odczyt z Kafka) z domenową (zmiana statusu zamówienia). Skutek: trudne testy, problemy z retry, brak idempotencji.
Spróbuj rozbić to na warstwy:
- warstwa adaptera – metoda oznaczona
@KafkaListener, która przyjmuje surową wiadomość (payload + headers), - warstwa aplikacyjna – serwis, który zna przypadki użycia (np.
orderPaymentService.handlePaymentAuthorized(...)), - warstwa domenowa – agregaty, encje, logika biznesowa, które nie wiedzą nic o Kafka.
Pomyśl, co się stanie, jeśli za rok zmienisz Kafka na inne rozwiązanie lub dodasz drugi kanał (np. REST callback z zewnętrznego systemu). Jeśli logika jest zamknięta w domenie, wymieniasz tylko adapter.
Druga kwestia: jak ustawiasz concurrency w @KafkaListener? Jeśli od razu ustawisz 20 wątków „bo tak szybciej”, a baza nie wyrabia, całość zacznie się dusić. Zadaj sobie pytanie: gdzie jest najsłabsze ogniwo – Kafka czy baza?
Batch processing i manualne commitowanie offsetów
Jeśli ruch rośnie, zaczniesz myśleć o batchowym przetwarzaniu. Spring Kafka wspiera batch listeners, ale trzeba je świadomie używać.
Główne decyzje:
- Rozmiar batcha – zbyt duży zwiększy opóźnienia, zbyt mały nie da zysku wydajności.
- Manualne commitowanie offsetów – po przetworzeniu całego batcha lub pojedynczych wiadomości.
- Strategia częściowego sukcesu – co jeśli w batchu jedna wiadomość jest uszkodzona?
Rozsądna strategia na start: przetwarzasz batch w pętli, a w przypadku błędu dla pojedynczej wiadomości wysyłasz ją do DLT (osobny producent) i przetwarzasz kolejne. Offset commitujesz po zakończeniu całej serii. Zadaj sobie pytanie: czy biznes godzi się na pomijanie „złych” rekordów, jeśli pozostałe są poprawne?
Monitoring i observability dla Spring Kafka
Bez sensownych metryk Kafka szybko staje się „czarną skrzynką”. W Spring Boot masz kilka narzędzi pod ręką (Micrometer, Actuator), trzeba je tylko spiąć.
Na co patrzeć na dashboardzie?
- lag konsumentów (opóźnienie między produkcją a konsumpcją),
- liczbę błędów i retry, osobno dla błędów technicznych i biznesowych,
- czas przetwarzania pojedynczego zdarzenia,
- wielkość batchy i czas commitów offsetów.
Zadaj sobie pytanie: czy potrafisz odpowiedzieć, jak długo klient czeka od wysłania zamówienia do wystawienia faktury? Jeśli nie, dorzuć trace-id (np. w nagłówkach Kafka) i zepnij to z systemem distributed tracing (Zipkin, Jaeger, Tempo).
Kubernetes jako platforma dla mikroserwisów – jak o tym myśleć
Czy naprawdę potrzebujesz Kubernetes na start?
Zanim zaczniesz tworzyć manifesty YAML, odpowiedz szczerze: jaki masz ruch, jak często releasujesz, ile masz serwisów?
Kubernetes daje:
- skalowanie poziome (więcej replik serwisu),
- odporność na awarie node’ów,
- spójny model konfiguracji i deploymentów,
- możliwość automatyzacji (GitOps, CI/CD).
Jeśli jednak masz jeden lub dwa serwisy i release raz w miesiącu, K8s może być armatą na muchę. Ale jeśli planujesz kilkanaście mikroserwisów, kilka środowisk (dev, test, prod) i częste releasy, zacznij myśleć o nim jak o „systemie operacyjnym dla aplikacji”.
Zadaj sobie pytanie: kto będzie tym systemem zarządzał? Jeśli nie masz zespołu, który rozumie Kubernetes, zacznij małą, wydzieloną częścią systemu, a nie całym krajobrazem produkcyjnym od razu.
Modelowanie mikroserwisu na Kubernetes: Deployment, Service, ConfigMap
Pojedynczy serwis Spring Boot ląduje na K8s zwykle jako zestaw obiektów:
- Deployment – opisuje, ile replik (podów) serwisu ma działać i jak je uruchamiać,
- Service (ClusterIP/LoadBalancer) – zapewnia stabilny DNS i adres dla ruchu,
- ConfigMap i Secret – konfiguracja i dane wrażliwe,
- opcjonalnie HorizontalPodAutoscaler – automatyczne skalowanie w oparciu o metryki.
Dla Spring Boot kluczowe jest przeniesienie konfiguracji z application.yml do ConfigMap i Secret oraz ustawienie health-checków (livenessProbe i readinessProbe) na endpointy Actuatora.
Zadaj sobie pytanie: czy twój serwis startuje w 2 sekundy czy w 40? Od tego będzie zależało ustawienie initialDelaySeconds w probe’ach i timeoutów w readiness. Jeśli pomylisz się w dół, Kubernetes uzna, że serwis „nie żyje” i zacznie go zapętlać.
Skalowanie mikroserwisów: co naprawdę skaluje się w górę?
W Javie pierwszym odruchem jest: „więcej replik = większa wydajność”. Tylko że mikroserwis to nie tylko CPU. Masz jeszcze:
- połączenia do bazy danych (pool),
- połączenia do Kafka (producenci i konsumenci),
- zewnętrzne API (limity rate limiting po stronie partnerów).
Zadaj sobie pytanie: jeśli ustawisz 10 replik serwisu, a każda ma pool 50 połączeń do Postgresa, jaki będzie efekt? Baza nagle musi obsłużyć 500 połączeń. Czy jest na to gotowa?
Przy soketach do Kafka pamiętaj: konsumentów w grupie może być więcej niż partycji, ale wtedy część będzie bez zajęcia. Lepiej dopasować liczbę replik serwisu-konsumenta do liczby partycji topiku, niż bezmyślnie ją mnożyć.
Spring Boot + Kubernetes: zdrowie, readiness i graceful shutdown
W świecie mikroserwisów ważniejsze staje się nie to, czy aplikacja „działa”, tylko czy jest gotowa do obsługi ruchu. Tutaj wchodzą w grę:
Jeśli interesują Cię konkrety i przykłady, rzuć okiem na: Strategie ładowania danych w Hibernate – lazy, eager i jak nie zabić wydajności aplikacji.
- livenessProbe – czy proces żyje (np.
/actuator/health/liveness), - readinessProbe – czy może przyjmować ruch (np.
/actuator/health/readiness).
W Spring Boot od wersji 2.3 można łatwo rozdzielić te dwa typy health-checków. Zadaj sobie pytanie: kiedy aplikacja powinna być „not ready”? Na przykład:
- brak połączenia z bazą,
- brak połączenia z brokerem Kafka,
- długi rozruch (np. warm-up cache).
Druga sprawa to graceful shutdown. Jeśli Kubernetes ubije poda w trakcie przetwarzania zdarzenia Kafka, możesz dostać duplikaty (offset nie został zapisany) albo utracić rezultat (transakcja w bazie nie została domknięta). Skonfiguruj w Spring Boot:
- czas na zamknięcie (property
spring.lifecycle.timeout-per-shutdown-phase), - graceful stop dla listenerów Kafka (Spring Kafka obsługuje to natywnie),
- czas
terminationGracePeriodSecondsw manifestach K8s.
Zadaj sobie pytanie: ile realnie potrzebujesz sekund, aby bezpiecznie zakończyć wszystkie aktywne requesty i wiadomości? Zmierz to na testach obciążeniowych, nie zgaduj.
Konfiguracja Spring Boot przez ConfigMap i Secret
Kiedy serwisów jest kilka, trzymanie konfiguracji w jednym application.yml przestaje wystarczać. Kubernetes pozwala rozdzielić konfigurację per środowisko.
Typowy układ:
- ConfigMap dla „niewrażliwych” rzeczy: adres brokera, nazwy topików, feature flagi,
- Secret dla haseł do bazy, tokenów do zewnętrznych API, kluczy do Schema Registry.
Spring Boot dobrze współpracuje z SPRING_APPLICATION_JSON oraz z plikami montowanymi jako wolumeny. Masz więc dwie drogi: wstrzykiwanie konfiguracji przez zmienne środowiskowe lub jako pliki. Zadaj sobie pytanie: co będzie łatwiejsze dla developerów i dla CI/CD?
Zadbaj też o spójne nazewnictwo kluczy, np.:
ORDER_SERVICE_KAFKA_BOOTSTRAP_SERVERS,ORDER_SERVICE_DB_URL,ORDER_SERVICE_DB_USERNAME.
Im bardziej przewidywalne nazwy, tym mniej „magii” w konfiguracji Helm/GitOps.
Logowanie i tracing w klastrze Kubernetes
Jeden mikroserwis możesz debugować patrząc w logi z jednego poda. Przy dziesięciu i więcej – bez centralnego logowania zaczyna się loteria.
Do spięcia logów z K8s możesz użyć:
- ELK/EFK (Elasticsearch + Fluentd/Fluent Bit + Kibana),
- Loki + Promtail + Grafana,
- komercyjnych rozwiązań SaaS (Datadog, Splunk, New Relic).
Zadaj sobie pytanie: czy jesteś w stanie prześledzić jedną transakcję biznesową przez kilka serwisów i kilka topików Kafka? Jeśli nie, dorzuć:
- trace-id w nagłówku HTTP (np.
X-Trace-Id) i w nagłówkach Kafka, - integrację z OpenTelemetry (biblioteki dla Spring Boot),
- spójny format logów (JSON z polami
traceId,spanId,service).
Wtedy w Kibanie czy Grafanie możesz filtrować logi po traceId i widzisz całą ścieżkę – od wejścia REST, przez wyprodukowane zdarzenia, po konsumpsję w kolejnych serwisach.
Deployment strategii: RollingUpdate, Canary, Blue/Green
Jedną z rzeczy, które Kubernetes wnosi „w pakiecie”, są strategie wdrożeń. Jeśli zmieniasz kontrakt zdarzeń lub logikę, przydaje się możliwość stopniowego wprowadzania zmian.
Masz kilka opcji:
- RollingUpdate – domyślna strategia, stopniowa wymiana podów na nowe,
- Canary – część ruchu idzie do nowej wersji (np. przez Istio/Linkerd),
- Blue/Green – dwie oddzielne wersje, przełączenie ruchu w jednym kroku.
Zadaj sobie pytanie: co się stanie, jeśli połowa serwisów jest w wersji „starej”, a połowa w „nowej”, z inną wersją eventu? Wtedy schematy zdarzeń muszą być kompatybilne (backward/forward). RollingUpdate jest bezpieczny, tylko jeśli zadbałeś o ewolucję kontraktów.
Dla bardziej wrażliwych zmian w mikroserwisach event-driven często stosuje się pattern:
- nowa wersja zdarzenia (
OrderCreatedV2) publikowana równolegle, - konsumenci najpierw uczą się obsługi obu wersji,
- po okresie przejściowym stara wersja zostaje wycofana.
Tu właśnie przydają się canary deployments – część loadu dostaje nową wersję, obserwujesz metryki, sprawdzasz DLT, dopiero potem rozszerzasz rollout.
Kafka w Kubernetes: gdzie ją trzymać i jak się łączyć?
Wielu zespołów od razu próbuje stawiać własny klaster Kafka na tym samym K8s. Czasem to ma sens, czasem lepiej użyć zarządzanej usługi (MSK, Confluent Cloud, Aiven).
Najczęściej zadawane pytania (FAQ)
Kiedy mikroserwisy w Javie ze Spring Boot i Kubernetes mają sens, a kiedy lepiej zostać przy monolicie?
Najpierw zadaj sobie pytanie: jaki masz cel? Jeśli potrzebujesz niezależnych wdrożeń, różnego skalowania dla poszczególnych obszarów (np. osobno dla zamówień, osobno dla płatności) i masz kilka zespołów, mikroserwisy zaczną się spłacać. Gdy domena jest złożona i wyraźnie podzielona na obszary biznesowe, rozbicie na mniejsze serwisy porządkuje odpowiedzialności.
Jeśli jednak zespół liczy 2–3 osoby, domena dopiero się kształtuje i każda zmiana dotyka prawie wszystkich części systemu, lepszą opcją jest modularny monolit. Możesz wtedy od razu wyznaczyć granice domen (moduły), ale wdrażać wszystko jako jedną aplikację. Mikroserwisy zostaw na moment, gdy naprawdę poczujesz ból: konflikty merge, brak skalowania wybranych modułów, problemy z niezależnymi wdrożeniami.
Jak zdecydować, które moduły monolitu wydzielić do osobnych mikroserwisów?
Usiądź z kartką i wypisz 3–5 miejsc, gdzie dziś najbardziej boli: wydajność, awaryjność, zależności wdrożeniowe. Potem zadaj sobie pytanie: „czy ten problem da się odizolować w osobnym procesie, który ma własną bazę i API lub zdarzenia?”. Jeśli tak, to kandydat na mikroserwis.
Dobrym tropem są procesy biznesowe, które:
- mają własne tempo zmian (np. płatności zmieniają się częściej niż katalog produktów),
- skalują się inaczej niż reszta (np. raportowanie dusi bazę transakcyjną),
- często kładą cały system, choć logicznie są tylko „dodatkiem” (np. fakturowanie).
Jeśli nie umiesz opisać obszaru zdaniem „Serwis X odpowiada za …” kończąc czasownikiem (np. „realizację zamówień”), tylko „za dane o …”, to zwykle za wcześnie na wydzielenie.
Jak praktycznie zastosować Domain-Driven Design przy projektowaniu mikroserwisów w Spring Boot?
Zacznij od języka, nie od technologii. Jakie trzy główne procesy biznesowe są kluczowe? Zamówienia, płatności, fakturowanie, katalog, użytkownicy? Nazwij te obszary i narysuj prostą mapę: kilka prostokątów (bounded contexts) i strzałki między nimi. Jeśli nie potrafisz zrobić tego w 10 minut, zatrzymaj się i doprecyzuj domenę.
Potem przełóż to na kod Javy:
- każdy bounded context = osobny mikroserwis lub moduł (np.
order,payment,invoice), - w środku konsekwentnie używaj języka biznesu w nazwach klas, metod, zdarzeń (nie mieszaj „Order” i „Purchase” dla tego samego pojęcia),
- nie współdziel encji JPA między kontekstami – każdy serwis ma własny model i własną bazę, integracja idzie przez REST lub zdarzenia z Kafki.
Zadaj sobie pytanie: „czy biznes, patrząc na nazwy pakietów i klas, zrozumiałby, co się tu dzieje?”. Jeśli nie – dopracuj ubiquitous language.
Jak podzielić domenę na mikroserwisy: funkcjonalnie, po danych czy wokół zdarzeń w Kafka?
Masz kilka osi i możesz je mieszać. Najczęściej startuje się od podziału funkcjonalnego: serwis Zamówienia, serwis Płatności, serwis Faktur, serwis Katalogu. Do tego dochodzi perspektywa zdarzeń: serwis Zamówienia emituje np. OrderCreated, a Płatności i Faktury reagują jako subskrybenci Kafki. W praktyce: co ma być źródłem prawdy, a co tylko reagującym słuchaczem?
Podział „wokół danych” (serwis = tabela) kusi, ale szybko prowadzi do CRUD-owych „mikromonolitów”, które niczego nie izolują. Dlatego najpierw zapytaj: „jaki proces lub odpowiedzialność ten serwis bierze na siebie?”. Jeśli jedyne uzasadnienie brzmi „tam trzymamy dane o produktach”, to potrzebujesz jeszcze doprecyzować zachowania: rejestrację, aktualizację, walidacje, reguły cenowe, dostęp innych kontekstów.
Jaką wewnętrzną architekturę mikroserwisu w Spring Boot wybrać, żeby dało się go łatwo rozwijać i testować?
Najbezpieczniejszym wyborem na start jest prosty wariant architektury heksagonalnej lub clean architecture. Podziel serwis na kilka czytelnych warstw:
- API – kontrolery REST/GraphQL, wejściowe kanały Kafki,
- warstwa aplikacyjna – use case’y, serwisy aplikacyjne, orkiestracja procesów,
- domena – encje, agregaty, logika biznesowa, obiekty wartości,
- infrastruktura – repozytoria, adaptery do Kafki, integracje zewnętrzne.
Zapytaj siebie: „czy jestem w stanie przetestować logikę domenową bez podnoszenia Springa i bez bazy?”. Jeśli nie, warstwy są najpewniej zbyt mocno posklejane.
Taki podział pomaga też przy migracji: łatwiej wyciąć jeden moduł z monolitu i opakować go jako mikroserwis, gdy logika domenowa nie jest wymieszana z kontrolerami i repozytoriami.
Kiedy w architekturze mikroserwisów z Javą warto użyć Kafki, a kiedy wystarczy REST?
Zadaj sobie dwa pytania: „czy ten proces musi być synchroniczny?” oraz „czy system ma reagować na zdarzenia, czy tylko udostępniać dane?”. REST sprawdza się, gdy:
- potrzebujesz natychmiastowej odpowiedzi (np. sprawdzenie dostępności produktu w koszyku),
- interakcja jest prosta: zapytanie → odpowiedź.
Kafka zyskuje sens, gdy:
- miejsce i czas przetwarzania możesz oderwać od miejsca i czasu wywołania (np. faktura może powstać kilka sekund po złożeniu zamówienia),
- wiele serwisów ma reagować na to samo zdarzenie (płatności, fakturowanie, raportowanie),
- chcesz mieć historię zdarzeń domenowych i łatwiej skalować konsumentów.
Jeśli łapiesz się na tym, że wszystkie wywołania między serwisami są synchroniczne i kaskadowe, zadaj pytanie: „które z nich to tak naprawdę reakcje na zdarzenia biznesowe?”. To naturalni kandydaci do przeniesienia na Kafkę.
Najważniejsze punkty
- Zanim zaczniesz rozbijać system na mikroserwisy w Javie, odpowiedz sobie, jaki masz główny cel: skalowanie, niezależne wdrożenia, czy odseparowanie odpowiedzialności zespołów – od tej decyzji zależy cały dalszy projekt.
- Mikroserwisy mają sens przy złożonej domenie, większych zespołach i potrzebie różnego skalowania poszczególnych obszarów (np. katalog vs płatności); przy małym zespole i słabo poznanej domenie lepszy jest modularny monolit.
- Przed migracją wypisz 3–5 najdotkliwszych problemów obecnego systemu i sprawdź, czy naprawdę dotyczą skalowania, stabilności i niezależności wdrożeń, czy raczej jakości kodu i testów – w drugim przypadku mikroserwisy tylko zwielokrotnią chaos.
- Ustal główne domeny biznesowe (np. zamówienia, płatności, fakturowanie, katalog, użytkownicy, raportowanie) i spróbuj narysować je na jednej kartce jako prostokąty z przepływami – jeśli tego nie potrafisz, architektura mikroserwisów będzie przypadkowa.
- Stosuj zasady DDD w wersji praktycznej: każdy bounded context to oddzielny serwis lub moduł, z własnym modelem danych i językiem, bez współdzielenia encji JPA między kontekstami; integracja odbywa się przez REST lub zdarzenia (np. w Kafka).
- Buduj ubiquitous language w kodzie: nazwy klas, metod, endpointów i zdarzeń mają odpowiadać językowi biznesu („Zamówienie”, „Płatność”, „Faktura”), a nie przypadkowym skrótom – zadaj sobie pytanie, czy analityk biznesowy zrozumie te nazwy bez tłumaczenia.






