Asynchroniczne integracje systemów w Javie z użyciem kolejek i JMS

0
72
Rate this post

Z tej publikacji dowiesz się:

Dlaczego asynchroniczne integracje? Kontekst, problemy i korzyści

Integracja synchroniczna a asynchroniczna – dwa różne światy

Integracja synchroniczna (typowo REST/HTTP) opiera się na bezpośrednim wywołaniu – klient wysyła żądanie i czeka na odpowiedź. Dopóki odpowiedź nie przyjdzie (lub nie nastąpi time-out), wątek jest zajęty, a użytkownik lub inny system „zamrożony” w oczekiwaniu. W klasycznym monolicie lub prostych aplikacjach B2C taki model bywa wystarczający, a często po prostu najprostszy.

Asynchroniczna komunikacja systemów wprowadza trzeci element – brokera wiadomości (message broker) i kolejki komunikatów.
Producent wysyła wiadomość do kolejki, a konsument odbiera ją niezależnie, zwykle w innym czasie i często na innych zasobach obliczeniowych.
Nadawca nie musi wiedzieć, kto i kiedy wiadomość przetworzy. Taka integracja silniej rozluźnia sprzężenie między systemami.

Różnica jest dobrze widoczna na prostym przykładzie: serwis zamówień musi poinformować serwis fakturowania o nowym zamówieniu.
W modelu REST serwis zamówień wywołuje endpoint serwisu fakturowania i czeka. W modelu asynchronicznym wysyła komunikat
„OrderCreated” do kolejki lub tematu, a serwis fakturowania odbierze go, gdy będzie gotowy. Dla serwisu zamówień operacja
kończy się po poprawnym opublikowaniu komunikatu.

Problemy przy silnym sprzężeniu systemów REST/HTTP

Silne sprzężenie w integracjach synchronicznych pojawia się wtedy, gdy łańcuch usług musi przejść w całości, aby odpowiedź wróciła do inicjatora.
Jeśli każda usługa woła następną synchronicznie, powstaje „łańcuch zależności czasowej”. Awaria lub spowolnienie jednego elementu blokuje wszystkie kolejne.

Typowe problemy to:

  • Zależność od dostępności – jeśli jedna usługa jest niedostępna, cały proces zatrzymuje się lub kończy błędem.
  • Kaskadowe awarie – problemy perfomance jednego serwisu powodują lawinę timeoutów w pozostałych.
  • Skokowe obciążenia – nagły wzrost ruchu użytkowników natychmiast uderza w każdy element łańcucha bez możliwości „wygładzenia”.
  • Złożone zarządzanie retry – ponawianie zapytań przez klientów może dodatkowo obciążać systemy i multiplikować problem.

Asynchroniczne kolejki komunikatów Java działają tutaj jak bufor i bezpieczny zawór. Opóźnienie w jednym systemie nie musi natychmiast rozwalić reszty łańcucha.
Nadawca wysyła komunikat i może przejść do obsługi kolejnych zadań, a backlog w kolejce przechwyci okresowe wzrosty ruchu.

Korzyści z asynchroniczności: bufor, odporność, zasoby

Asynchroniczna integracja przez JMS Java Message Service i kolejki przynosi trzy główne kategorie korzyści: kontrolę przepływu,
odporność na awarie i lepsze wykorzystanie zasobów. Każda z nich ma wymiar bardzo praktyczny.

Po pierwsze, kolejki pełnią rolę bufora obciążenia. Jeżeli nagle spływa więcej zadań (np. zamówień) niż konsument jest
w stanie przetworzyć w jednostce czasu, zadania nie giną – rośnie długość kolejki. Konsument może skalować się poziomo lub nadrabiać kiedy zasoby są dostępne.
Z perspektywy nadawcy system „nie pęka” w momencie skoku ruchu.

Po drugie, integracja asynchroniczna zwiększa odporność na awarie. Jeśli konsument jest czasowo niedostępny:

  • komunikaty mogą czekać w kolejce (przy odpowiedniej konfiguracji trwałości),
  • nadawca nie musi natychmiast znać wyniku – wystarczy informacja, że komunikat został przyjęty przez brokera,
  • po powrocie konsumenta komunikaty zostaną stopniowo przetworzone.

Po trzecie, asynchroniczność poprawia wykorzystanie zasobów. W modelu synchronicznym wątki blokują się na zewnętrznych I/O,
zużywając pamięć i sloty w puli wątków. W modelu messagingowym często da się zastosować oddzielne pule konsumentów, inne języki, a nawet inne technologie,
bez wpływu na interfejsy nadawcy. Serwis webowy może szybko przyjąć żądanie, zapisać zadanie do kolejki i zwolnić wątek HTTP.

Gdzie kolejki wygrywają z REST w praktyce

Asynchroniczna komunikacja przez JMS szczególnie dobrze sprawdza się w kilku kategoriach scenariuszy biznesowych:

  • Operacje wolne lub o nieprzewidywalnym czasie trwania, jak wysyłka e-maili, generowanie raportów PDF, integracje z zewnętrznymi API marketingowymi.
  • Integracje z systemami legacy, które pracują nierówno, mają okna serwisowe lub ograniczone limity zapytań.
  • Zdarzenia domenowe (np. OrderCreated, InvoicePaid), które konsumuje wiele systemów – kolejki/tematy naturalnie obsługują jeden-do-wielu.
  • Procesy, które nie wymagają natychmiastowej odpowiedzi dla użytkownika – np. „Twoje zamówienie jest w trakcie realizacji”.

Przykład z życia: sklep internetowy po przyjęciu zamówienia robi kilka rzeczy – wysyła maila, zgłasza zamówienie do systemu magazynowego,
aktualizuje CRM, wysyła dane do systemu analitycznego. Zamiast czterech synchronicznych wywołań, które muszą przejść, by klient zobaczył stronę „Dziękujemy”,
lepiej odłożyć te działania do kolejki i dać użytkownikowi odpowiedź natychmiast po utworzeniu rekordu zamówienia.

Kiedy asynchroniczność jest nadmiarem

Nie każdy przypadek uzasadnia wprowadzenie JMS, brokera i złożonej infrastruktury messagingowej.
Dla prostych operacji typu „pobierz dane i natychmiast wyświetl” integracja synchroniczna REST/HTTP bywa zwyczajnie lepsza.
Szczególnie gdy:

  • wymagana jest natychmiastowa odpowiedź dla użytkownika,
  • zapytania są niewielkie i rzadkie,
  • strony nie przetwarzają krytycznych zdarzeń biznesowych, a jedynie robią „read-only lookups”.

Asynchroniczna architektura messagingowa wprowadza nowe elementy do utrzymania: broker, monitoring, zarządzanie DLQ,
ciągły nadzór nad backlogiem komunikatów. Jeśli komunikacja jest prosta, jednowymiarowa i ma małe wymagania niezawodnościowe,
koszt tej złożoności może przewyższyć korzyści.

Serwer z podłączonymi żółtymi i zielonymi przewodami na ciemnym tle
Źródło: Pexels | Autor: Brett Sayles

Podstawy messagingu w Javie – pojęcia i komponenty

Kluczowe pojęcia w świecie kolejek

Podstawowe elementy ekosystemu messagingowego w Javie i JMS są dość uniwersalne, niezależnie od wybranego brokera.
Najczęściej spotykane pojęcia to:

  • Message (komunikat) – jednostka danych przesyłana przez system, może zawierać nagłówki (headers/properties) i ciało (payload).
  • Producer (producent) – aplikacja lub komponent wysyłający komunikaty do kolejki lub tematu.
  • Consumer (konsument) – aplikacja lub komponent odbierający komunikaty z kolejki lub subskrypcji.
  • Destination (destination) – logiczny cel komunikatu, zazwyczaj Queue albo Topic.
  • Message Broker (broker) – serwer pośredniczący w dostarczaniu komunikatów między producentami i konsumentami.

W JMS obiekty typu ConnectionFactory, Connection, Session, MessageProducer, MessageConsumer i Destination
zapewniają standardowy sposób tworzenia połączeń, sesji i wysyłania / odbierania komunikatów bez zależności od konkretnego brokera.

Model point-to-point kontra publish/subscribe

W integracjach asynchronicznych dominują dwa podstawowe style:

  • Point-to-point – komunikacja przez Queue, czyli kolejkę.
  • Publish/Subscribe – komunikacja przez Topic, czyli kanał publikacji/subskrypcji.

Model point-to-point realizuje scenariusz „jeden komunikat – jeden konsument”. Producent wysyła wiadomość do kolejki, a wielu konsumentów
może nasłuchiwać na tej kolejce, ale każdy konkretny komunikat zostanie dostarczony tylko jednemu z nich. Ten model idealnie pasuje do zadań,
które są niezależne i mają być równomiernie rozdzielane między procesory (workers), np. zlecenia generowania dokumentów.

Model publish/subscribe zakłada rozgłaszanie – komunikat jest publikowany na temat (topic),
a każdy aktywny subskrybent otrzymuje swoją kopię. Nadaje się to do zdarzeń domenowych, gdzie wiele systemów ma zareagować
na jedno zdarzenie: CRM, billing, logistyka, analityka, itd. W JMS istnieje też pojęcie subskrypcji trwałych (durable subscriptions),
które pozwalają „nadrobić” zdarzenia, gdy subskrybent był chwilowo offline.

Trwałość komunikatów i wymagania biznesowe

JMS rozróżnia komunikaty persistent i non-persistent.
To nie tylko detal techniczny, ale przede wszystkim decyzja biznesowo-architektoniczna.

  • Persistent – komunikat jest zapisywany przez brokera w sposób trwały (zwykle na dysku) przed potwierdzeniem do producenta.
    Zwiększa to niezawodność kosztem wydajności.
  • Non-persistent – komunikat może zostać utracony w razie awarii brokera, ale przepustowość jest wyższa, a opóźnienia mniejsze.

Scenariusze krytyczne – jak zlecenia finansowe, zamówienia, ważne zdarzenia księgowe – powinny używać komunikatów trwałych.
Z kolei logi zdarzeń, dane telemetryczne czy powiadomienia o niewielkim znaczeniu często można wysyłać jako nietrwałe, z naciskiem na przepustowość.
Dobrym podejściem jest świadome oznaczenie każdego kanału komunikacji stopniem krytyczności i powiązanie go z polityką trwałości.

JMS a natywni klienci brokerów i alternatywne technologie

JMS jest standardem Java (Java Message Service), a nie konkretnym produktem.
W praktyce oznacza to, że istnieje wiele implementacji JMS dla różnych brokerów: ActiveMQ, ActiveMQ Artemis, IBM MQ, OpenMQ i inne.
Jednocześnie większość współczesnych brokerów oferuje natywne protokoły i klientów: AMQP, MQTT, STOMP, protokoły specyficzne dla danego narzędzia.

Porównując:

  • JMS – standard Java API, dobra integracja z Javą, przenośny kod między brokerami (z pewnymi zastrzeżeniami), prosty model programistyczny.
  • AMQP / MQTT / natywni klienci – lepsze wsparcie poza Javą, specyficzne funkcje brokera, często lepsza wydajność dla danego przypadku, ale kosztem przenośności kodu.
  • Kafka – osobna kategoria; event log z inną semantyką (partition, offset, consumer groups), mocno nastawiony na streaming danych.

W systemach mocno opartych na Javie JMS pozostaje naturalnym wyborem, szczególnie gdy kluczowa jest integracja w ekosystemie Java EE/Jakarta EE lub Spring.
Kiedy ekosystem jest wielojęzyczny lub wymagana jest konkretna funkcjonalność (np. natywne AMQP w RabbitMQ), rozsądniejsze może być użycie natywnych klientów.

Granice odpowiedzialności brokera i aplikacji

Dobrze zaprojektowana integracja asynchroniczna rozdziela odpowiedzialności między brokerem a kodem aplikacji.
Z praktyki:

  • Broker odpowiada za: kolejki, routing, trwałość komunikatów, dostarczenie ich do dostępnych konsumentów, podstawowe retry i DLQ.
  • Aplikacja odpowiada za: logikę biznesową, idempotencję przetwarzania, walidację payloadu, obsługę błędów domenowych.

Umieszczanie nadmiernie skomplikowanych reguł w konfiguracji brokera (np. złożony routing na bazie wielu właściwości biznesowych) szybko prowadzi do trudnego w utrzymaniu „middleware potwora”.
Z drugiej strony przerzucanie na kod aplikacji wszystkiego (włącznie z manualnym ponawianiem dostarczenia, re-queuingiem, itp.) też jest drogą do złożoności.
Rozsądny podział zwykle oznacza użycie funkcji brokera do technicznego zapewnienia dostarczenia oraz delegowanie decyzji biznesowych do aplikacji.

JMS w praktyce – API, implementacje i wybór technologii

Przypomnienie specyfikacji JMS 1.x i JMS 2.0

JMS 1.x był przez lata podstawą messagingu w Javie. Wymagał sporo „ceremonii” – tworzenia connection, session, producer/consumer,
często z użyciem JNDI i kodu szablonowego. JMS 2.0 wprowadził uproszczone API, m.in. obiekt JMSContext i fabryki sesji,
które ograniczają liczbę linii kodu potrzebnych do wysłania prostego komunikatu.

Najważniejsze cechy JMS jako specyfikacji:

  • Abstrakcja nad brokermi – ten sam kod może działać z różnymi dostawcami.
  • Obsługa kolejek (Queue) i tematów (Topic) w jednym modelu.
  • Wsparcie dla transakcji lokalnych (session transacted) i integracji z transakcjami XA.
  • Różne typy komunikatów (TextMessage, BytesMessage, ObjectMessage, MapMessage) opisujące zarówno nagłówki techniczne, jak i dane biznesowe.
  • Mechanizmy filtrowania oparte na selektorach (message selectors), pozwalające konsumentowi odbierać wybrane komunikaty na podstawie właściwości nagłówków.
  • Tryby potwierdzania odbioru (acknowledgement modes), wpływające na gwarancje dostarczenia i sposób zarządzania błędami.

W codziennej pracy różnica między JMS 1.x a 2.0 najlepiej wychodzi przy prostych przypadkach: w 1.x potrzebny jest szereg wywołań do stworzenia sesji,
producenta i samej wiadomości, w 2.0 sprowadza się to do kilku linii z użyciem JMSContext. Przy bardziej rozbudowanych scenariuszach (transakcje XA,
niestandardowe polityki dostarczania, zaawansowane bezpieczeństwo) uproszczone API i tak odwołuje się do tych samych mechanizmów pod spodem, więc
decyzja sprowadza się do ergonomii kodu, a nie do różnic funkcjonalnych.

Popularne implementacje JMS i ich charakterystyka

Przy wyborze konkretnego brokera z obsługą JMS najczęściej pojawiają się trzy nazwy: ActiveMQ Classic, ActiveMQ Artemis oraz IBM MQ.
Każdy z nich celuje w nieco inny zestaw priorytetów.

ActiveMQ Classic jest prosty w uruchomieniu, ma bardzo dobry ekosystem narzędzi i przykładów, lecz gorzej skaluje przy dużym obciążeniu
i intensywnym routingowaniu. ActiveMQ Artemis projektowano już z myślą o wysokiej wydajności i niskich opóźnieniach; sprawdza się zarówno
w lekkich środowiskach kontenerowych, jak i w scenariuszach wymagających dużej przepustowości. IBM MQ z kolei stawia na stabilność,
długoterminowe wsparcie i rozbudowane możliwości integracji z infrastrukturą enterprise (mainframe, klastery HA, zaawansowane mechanizmy bezpieczeństwa).

W mniejszych projektach i środowiskach developerskich zazwyczaj wystarcza ActiveMQ Classic lub Artemis, dostępne jako obrazy Docker,
łatwe do podniesienia na laptopie czy w CI. W sektorze finansowym czy telekomunikacyjnym częściej dominuje IBM MQ, w dużej mierze z uwagi na
wymagania dot. wsparcia i certyfikacji. Tam, gdzie liczy się przede wszystkim wydajność i elastyczność, a mniej „enterprise’owy” charakter,
Artemis daje lepszy stosunek możliwości do złożoności konfiguracji.

JMS kontra alternatywy: kiedy standard, a kiedy rozwiązanie specyficzne

W projektach czysto javowych JMS nadal bywa najbardziej pragmatycznym wyborem: biblioteki są dojrzałe, narzędzia znane, a integracja z
Spring/Jakarta EE niemal bezkonfiguracyjna. W momencie, gdy w architekturze pojawia się kilka technologii (.NET, Node.js, Python)
i konieczna jest naprawdę równa obsługa wszystkich języków, lepiej wypadają protokoły z natury wieloplatformowe, takie jak AMQP w RabbitMQ
czy natywne API Kafki.

Warto przy tym odróżnić klasyczny messaging (kolejki, typowe request/response, prosty event-driven) od scenariuszy streamingu danych i analityki.
Dla tych drugich Kafka, Pulsar czy Redpanda często oferują znacznie lepsze dopasowanie: długotrwałe przechowywanie zdarzeń, re-odtwarzanie
strumieni, mechanizmy consumer groups. JMS w takich sytuacjach zwykle ląduje jako warstwa integracyjna na obrzeżach lub jest całkowicie
zastępowany przez „event log” jako główne źródło prawdy o zdarzeniach.

W efekcie architektury mieszane są dość częste: JMS i klasyczny broker do transakcyjnych kolejek oraz obsługi żądań biznesowych,
a Kafka lub podobne narzędzie do analityki zdarzeń, monitoringu i integracji z systemami raportowymi. Taki podział upraszcza decyzje:
JMS obsługuje „tu i teraz” procesów biznesowych, a streaming – długofalowe przetwarzanie danych i asynchroniczne modele raportowe.

Konkretny wybór bywa więc prosty, jeśli jasno określi się dominujący typ obciążenia. Przy dużej liczbie relatywnie małych, krytycznych biznesowo komunikatów korzystniej wypada klasyczny broker JMS z mocnym wsparciem transakcji i DLQ. Przy masowym strumieniowaniu zdarzeń telemetrycznych czy logów – Kafka lub podobny system logów zdarzeń. Sytuacje mieszane (np. zamówienia biznesowe + potok danych analitycznych) zwykle kończą się hybrydą dwóch technologii, a kluczowe staje się utrzymanie spójnego modelu zdarzeń i identyfikatorów korelacyjnych między światem JMS a światem streamingu.

W integracjach stricte system–system, gdzie komunikacja przypomina bardziej wywołania RPC niż swobodne publikowanie zdarzeń, JMS z podejściem request/response (kolejki odpowiedzi, korelacja przez JMSCorrelationID) nadal broni się lepiej niż Kafka. Implementacja wzorca „odpowiedź na kolejce tymczasowej” jest prosta, a semantyka dobrze odpowiada oczekiwaniu na pojedynczy wynik. Przy architekturach mocno event-driven, w których zdarzenia są stanem pierwszej klasy, event log ze swoim modelem partycji i offsetów daje z kolei większą elastyczność, zwłaszcza gdy trzeba odtwarzać historię i utrzymywać wiele niezależnych konsumentów.

W środowiskach regulowanych (bankowość, ubezpieczenia, sektor publiczny) częściej wygrywa JMS z „ciężkim” brokerem klasy IBM MQ – głównie z powodu dojrzałych mechanizmów bezpieczeństwa, obsługi transakcji rozproszonych i wieloletniego wsparcia producenta. W firmach produktowych, które rozwijają mikrousługi w kilku językach i chcą szybko eksperymentować z nowymi funkcjami, częściej pojawiają się RabbitMQ (AMQP) i Kafka, bo łatwiej je podłączyć do Node.js, Pythona czy Go bez dodatkowych warstw pośrednich.

Dobrym filtrem decyzyjnym jest odpowiedź na dwa pytania: czy większość logiki integracyjnej jest w Javie, oraz czy dominują krótkotrwałe, transakcyjne operacje. Jeśli tak, standard JMS i dedykowany broker rozwiązują większość problemów przy relatywnie niskim koszcie poznawczym. Gdy priorytetem jest pełna wielojęzyczność, bardzo wysoka skalowalność strumieni i długotrwałe przechowywanie zdarzeń – lepiej szukać rozwiązań natywnych dla danego ekosystemu messagingowego i traktować JMS najwyżej jako dodatkowy adapter.

Niezależnie od ostatecznego wyboru technicznego zestaw dobrych praktyk pozostaje zbliżony: jasny kontrakt komunikatu, przemyślana semantyka ponowień i błędów, rozsądny podział odpowiedzialności między brokerem a kodem oraz proste, powtarzalne wzorce architektoniczne. Java z JMS nadal daje solidny fundament pod takie integracje, a tam, gdzie pojawiają się inne narzędzia, dobrze zaprojektowany model zdarzeń pozwala spokojnie łączyć światy kolejek, logów zdarzeń i natywnych protokołów.

Światłowody podłączone do portów w nowoczesnym serwerze danych
Źródło: Pexels | Autor: Brett Sayles

Projektowanie kontraktu komunikatu – format, ewolucja, kompatybilność

Komunikat jako kontrakt między systemami

Dla jednego zespołu komunikat JMS bywa jedynie obiektem DTO rzuconym na kolejkę. Dla integracji kilku systemów staje się kontraktem między organizacjami lub domenami, który musi przeżyć lata zmian po obu stronach. Im luźniej jest zdefiniowany, tym taniej w krótkim terminie, ale tym drożej przy każdej zmianie.

Przy prostych integracjach w obrębie jednej domeny biznesowej często wystarczy JSON z kilkoma polami, bez formalnego schematu. Gdy po drugiej stronie jest inny dostawca, osobny zespół lub zewnętrzny partner, zwykle lepiej wypada podejście formalne: opis kontraktu w schemacie (XSD, JSON Schema, Avro), wersjonowanie, narzędzia do walidacji i testów kontraktowych.

Dwie skrajności, które regularnie się powtarzają:

  • „Message-as-entity” – kopiowanie pełnego modelu encji z bazy jako komunikatu. Łatwo wysłać, trudno rozwijać (każda zmiana encji wpływa na integrację).
  • „Message-as-event” – komunikat opisuje konkretne zdarzenie domenowe (np. OrderCreated) i zawiera tylko dane potrzebne odbiorcom. Więcej pracy przy projektowaniu, ale większa stabilność kontraktu.

Dla systemów silnie transakcyjnych i „request/response” częściej wygrywa model bliższy encjom (żądanie/odpowiedź z danymi podobnymi do REST). W architekturach event-driven lepszy jest komunikat jako zdarzenie z minimalnym, ale stabilnym zestawem pól.

Wybór formatu: JSON, XML, Avro i pozostałe opcje

Przy JMS wybór formatu nie jest narzucony. Ten sam TextMessage może nieść JSON, XML albo CSV. W praktyce dominują trzy podejścia:

  • JSON – wygodny dla developerów, prosty do debugowania, dobrze wspierany praktycznie w każdym języku.
  • XML – częsty w starszych integracjach enterprise, mocne wsparcie dla schematów (XSD), walidacji i narzędzi korporacyjnych.
  • Formaty binarne (Avro, Protobuf) – lepsza wydajność, mniejszy rozmiar, kosztem czytelności i prostoty ad-hoc.

JSON dobrze sprawdza się, gdy:

  • komunikują się głównie usługi webowe (REST + messaging),
  • integrację dotykają różne języki (Java, Node.js, Python),
  • debugowanie „na żywo” jest częste – logi, narzędzia admina, konsola brokera.

XML jest sensowny, gdy istnieje już otoczenie zbudowane wokół XSD: repozytoria schematów, walidatory, bramki ESB czy WSDL-e. Plusem jest bardzo precyzyjna walidacja; minusem – większa „ceremonia” i rozmiar komunikatu.

Formaty binarne typu Avro są bliższe światu Kafki, ale z JMS współpracują bez problemu (np. w BytesMessage). Dają przewagę, gdy:

  • komunikaty są liczne i duże (np. dane telemetryczne),
  • obciążenie sieci jest krytyczne,
  • istnieje już ekosystem ze schematami (schema registry) i narzędziami do generacji klas.

Dla typowej integracji system–system w Javie JSON jest często najbardziej pragmatycznym wyborem. XML lub Avro zaczynają wygrywać, gdy priorytetem staje się twarda walidacja schematu lub minimalizacja rozmiaru i narzutu serializacji.

Schemat i wersjonowanie – jak nie zablokować rozwoju

Problem nie pojawia się przy pierwszej wersji kontraktu, lecz przy trzeciej i piątej. Dwa najczęstsze scenariusze rozwoju:

  • Rozszerzenie komunikatu – dodanie nowych pól bez zmiany znaczenia dotychczasowych.
  • Zmiana semantyki – usunięcie lub modyfikacja istotnego pola, zmiana jednostek, inna logika.

Pierwszy scenariusz można utrzymać jako wstecznie kompatybilny, jeśli:

  • nowe pola są opcjonalne,
  • odbiorcy ignorują nieznane elementy (w JSON/Avro zazwyczaj naturalne),
  • stare pola nie zmieniają znaczenia.

Drugi scenariusz często wymaga nowej wersji komunikatu (np. OrderCreatedV2) lub osobnego typu zdarzenia. Próba „podmiany semantyki pod tym samym typem” zwykle kończy się koniecznością skomplikowa