Event sourcing, CQRS, a do tego message brokers

0

Cześć, ostatnio natchnęło mnie na poznanie tych dwóch podejść architektonicznych i jak zauważyłem często idą one ze sobą w parze. No i natknęło mnie na wiele pytań, czyli:

  1. Rozumiem, że przy połączeniu tego mamy dwie bazy: Command (Event Store(?)) oraz Query (gdzie trzymamy jedynie obecny stan), tak? W takim razie jeśli Command nam produkuje event to powoduje jednocześnie zapis do bazy Query. No i tutaj na obecnym stanie w bazie Query leci UPDATE czy INSERT, który historyzuje poprzedni stan, a nowy ustawia jako aktualny? Czy może wydzielić to w ogóle na 3 story (Command, Query, Event?). W dodatku ten nowy stan tworzymy na podstawie eventów czy na podstawie obecnego stanu? Przykład: konto bankowe ma 4500 zł, przychodzi event DepositMoney i teraz bierzemy obecny stan i dodajemy do niego value tego eventu (np. 500 zł?)
  2. Rozumiem, że taki event sourcing pozwala nam na to, że potrafimy odtworzyć każdy stan z przeszłości wskazując na odpowiedni event. W takich domyślnych javowych aplikacjach enterprise używamy do tego jakichś gotowców pokroju Axona lub Spring Application Events? Czy raczej pisze się takie coś samemu? Czy możemy np. do tego użyć samemu Kafki i np. w jakiś sposób persystować nasze eventy Kafkowe (tutaj pytanie jak? Jakiś NoSQL? Redis?).
  3. Czy lepiej wydzielić do tego odpowiednie narzędzia czyli Springowe Eventy/Axon? Jeśli tak to tutaj też jak najlepiej przechowywać te eventy? Natomiast Kafki używać tylko do sytuacji pokroju register -> sendConfirmationEmail gdzie register wysyła message, który mówi żeby wysłać email przez sendConfirmationEmail dając mu jako message ten obiekt? Ogólnie do jakich zadań w biznesowych aplikacjach stosujemy message brokery pokroju Rabbita czy Kafki (ujednolicam tutaj, wiem że różnice między tymi dwoma narzędziami są i nie do końca stosujemy je do tych samych sytuacji).

Pytam, bo technologicznie jestem zacofany w mojej pracy i planuję zmiany, a chcę się zorientować w tym temacie, bo wydaje się dość ciekawy :'(.
Jeśli możecie to podrzućcie przy okazji jakieś ciekawe prelekcje/książki/artykuły w tej tematyce.

1

Command wyraża intencję wykonania jakiejś operacji, a przy wykonywaniu rozgłasza eventy, które mówią o tym, co zaszło. Command może być zależny od obecnego stanu, zewnętrznych systemów itp. i nie ma sensu go zapisywać, przykład:
Command: wymień 100 EUR -> PLN
Event: wymieniono 100 EUR na PLN po kursie 4,42

Eventy zapisujesz w event storze i tak naprawdę to wystarczy, jeśli nie stanowi to problemu wydajnościowego to queries mogą czytać prosto z event stora, a dodatkowe read modele możesz dodawać w ramach potrzeb. Wtedy one nasłuchują eventów i aktualizują się na ich podstawie.

W dodatku ten nowy stan tworzymy na podstawie eventów czy na podstawie obecnego stanu? Przykład: konto bankowe ma 4500 zł, przychodzi event DepositMoney i teraz bierzemy obecny stan i dodajemy do niego value tego eventu (np. 500 zł?)

Po co za każdym razem czytać poprzednie eventy skoro wystarczy zrobić jeden UPDATE?

Co do kolejek, javy itp. się nie wypowiem, bo nie jestem javowcem ani nie robiłem ES na produkcji. Zwrócę tylko uwagę, że event sourcing dotyczy tylko tego, jak wygląda główne źródło prawdy dla aplikacji i nie ma to nic wspólnego z kolejkami, przesyłaniem pomiędzy mikroserwisami itp.

0

Command wyraża intencję wykonania jakiejś operacji, a przy wykonywaniu rozgłasza eventy, które mówią o tym, co zaszło. Command może być zależny od obecnego stanu, zewnętrznych systemów itp. i nie ma sensu go zapisywać, przykład:
Command: wymień 100 EUR -> PLN
Event: wymieniono 100 EUR na PLN po kursie 4,42

Czyli przy komendzie rozgłaszamy eventy, które zapisujemy do event store, a command nie jest w zasadzie w ogóle persystentne?

Eventy zapisujesz w event storze i tak naprawdę to wystarczy, jeśli nie stanowi to problemu wydajnościowego to queries mogą czytać prosto z event stora, a dodatkowe read modele możesz dodawać w ramach potrzeb. Wtedy one nasłuchują eventów i aktualizują się na ich podstawie.

No właśnie, czy takie Query prosto z event store nie jest dość powolne? No, bo tutaj w tym przypadku jeśli nie trzymamy nigdzie stricte stanu naszej encji to musimy zaciągnąć wszystkie eventy i na ich podstawie uzyskiwać obecny stan? Czy może tworzy się do tego wtedy jakieś Cache, które pozwalają nam np. ogarnąć stan naszej encji przy danym evencie w taki sposób żebyśmy nie musieli zaciągać całej historii, a jedynie od danego eventu?

Po co za każdym razem czytać poprzednie eventy skoro wystarczy zrobić jeden UPDATE?

Czyli jeśli mamy taki read model pod Query to tam mutujemy stan przez UPDATE na naszej stronie READ, tak? No tylko teraz to nie robi nam problemu dwóch źródeł prawdy? Co jeśli pójdzie event do event store, a insert do naszego read modelu się nie powiedzie?

Co do kolejek, javy itp. się nie wypowiem, bo nie jestem javowcem ani nie robiłem ES na produkcji. Zwrócę tylko uwagę, że event sourcing dotyczy tylko tego, jak wygląda główne źródło prawdy dla aplikacji i nie ma to nic wspólnego z kolejkami, przesyłaniem pomiędzy mikroserwisami itp.

Tak, wiem, wiem że kolejkowanie/komunikacja między mikroserwisami to jest inna bajka niż ES, jednak wolałem od razu tutaj też o to zapytać :D

1
lavoholic napisał(a):

Czyli przy komendzie rozgłaszamy eventy, które zapisujemy do event store, a command nie jest w zasadzie w ogóle persystentne?

W zasadzie tak.

No właśnie, czy takie Query prosto z event store nie jest dość powolne? No, bo tutaj w tym przypadku jeśli nie trzymamy nigdzie stricte stanu naszej encji to musimy zaciągnąć wszystkie eventy i na ich podstawie uzyskiwać obecny stan? Czy może tworzy się do tego wtedy jakieś Cache, które pozwalają nam np. ogarnąć stan naszej encji przy danym evencie w taki sposób żebyśmy nie musieli zaciągać całej historii, a jedynie od danego eventu?

Jeśli query dotyczy jednej encji, która ma kilkanaście czy nawet kilkadziesiąt eventów to ich odtworzenie może mieć pomijalny narzut. Zwykle jeśli aplikacja jest oparta standardowo na bazie SQL to też często na jedną encję biznesową leci wiele zapytań z joinami które swój koszt mają.

Czyli jeśli mamy taki read model pod Query to tam mutujemy stan przez UPDATE na naszej stronie READ, tak? No tylko teraz to nie robi nam problemu dwóch źródeł prawdy? Co jeśli pójdzie event do event store, a insert do naszego read modelu się nie powiedzie?

Nie znam się, no ale można próbować ponownie, skasować read model i przebudować od nowa itp.

0

Rozumiem, a w zasadzie co powinien taki Event zawierać? Id eventu, Id agregatu, timestamp? No, ale w zasadzie powinien też trzymać dane, co się dokładnie zmieniło no i o ile.
No, bo np. mamy coś takiego:

class User {
 UUID id;
 String login;
 String password;
 String email;
 
 User changePassword(PasswordChanged passwordChanged) {
     this.password = passwordChanged.getPassword();
     eventPublisher.publish(passwordChanged);
     return this;
 }

User changeEmail(EmailChanged emailChanged) {
     this.email = emailChanged.getEmail();
     eventPublisher.publish(emailChanged);
     return this;
 }
}

No i teraz, jak skladamy coś takiego do EventStore to jak jednoznacznie pokazywać gdy pobieramy to z tego eventstora, że dla danego User'a zmieniło się w tym evencie właśnie to pole i to z taką wartością? W dodatku co sprawdza się najlepiej jako EventStore? NoSQL, SQL, Key Value, czy może jakaś Kafka?

9

Ja zacznę od początku:

W takim razie jeśli Command nam produkuje event to powoduje jednocześnie zapis do bazy Query. No i tutaj na obecnym stanie w bazie Query leci UPDATE czy INSERT, który historyzuje poprzedni stan, a nowy ustawia jako aktualny? Czy może wydzielić to w ogóle na 3 story (Command, Query, Event?). W dodatku ten nowy stan tworzymy na podstawie eventów czy na podstawie obecnego stanu? Przykład: konto bankowe ma 4500 zł, przychodzi event DepositMoney i teraz bierzemy obecny stan i dodajemy do niego value tego eventu (np. 500 zł?)

Jak już wspomniał @mad_penguin, commands to jedynie zasygnalizowanie intencji. Między przetworzeniem command i opublikowaniem eventu należy dokonać walidacji, logikę biznesową itp. Wykonanie command może, a czasem nawet musi się nie powieść. Np. jeśli command to polecenia wybrania z konta sumy pieniędzy której na tym koncie nie ma. Event to z kolei informacja o tym co się stało w systemie, i sam w sobie nie może zostać odrzucony. Przy odtwarzaniu stanu agregatu (jeśli mowa o event sourcing to warto zapoznać się z ideą agregatów) event musi zostać zaakceptowany. Jeśli event wprowadza w błąd stan systemu z punktu widzenia zasad biznesowych, to należy opublikować kolejny event naprawiający ten stan (tzw. compensating event). Co za tym idzie, wszelkie "encje" (a tak naprawdę agregaty, a więc zbiór encji i value objects) powinny być odtwarzane za każdym razem (budowane) za pomocą eventów, ponieważ tylko to załadowanie eventów zapewnia że jest to aktualny stan obiektu. Queries niosą ze sobą eventual consistency, a więc nie ma gwarancji że są aktualne.

Punkty 2 i 3

Tutaj również za bardzo się nie wypowiem co do implementacji technicznej. Ogólnie najważniejsze jest aby każdy event był zawsze zapisywany (transakcyjność).To czy i jak później się go wyśle do kolejek ma drugorzędne znaczenie. Najważniejsze aby każdy event znalazł się w event store- nie ważne czy jest to jakaś dedykowana technologia (np. https://eventstore.com/) czy też własna implementacja. Można np. zastosować tzw. outbox pattern, a więc event zostaje zapisany do event store a następnie jakiś proces w tle emituje ten event do kolejki.

Co do tego kiedy stosuje się brokery- wtedy kiedy eventy (i czasem commands) muszą docierać do modułów poza procesem. Np. kiedy event opublikowany przez agregat A w serwisie A musi zainicjować jakiś proces biznesowy w agregacie B w serwisie B, zakładając że oba serwisy są niezależnymi od siebie procesami i komunikują się sieciowo.

Czyli przy komendzie rozgłaszamy eventy, które zapisujemy do event store, a command nie jest w zasadzie w ogóle persystentne?

Tak

No właśnie, czy takie Query prosto z event store nie jest dość powolne? No, bo tutaj w tym przypadku jeśli nie trzymamy nigdzie stricte stanu naszej encji to musimy zaciągnąć wszystkie eventy i na ich podstawie uzyskiwać obecny stan? Czy może tworzy się do tego wtedy jakieś Cache, które pozwalają nam np. ogarnąć stan naszej encji przy danym evencie w taki sposób żebyśmy nie musieli zaciągać całej historii, a jedynie od danego eventu?

Query to read-model, a więc używane jest np. do zwrócenia danych użytych do wyświetlania po stronie klienta. Query nie powinno być używane po stronie zapisów (commands) ponieważ jak już wspomniałem jest eventually consistent, a więc wykonywanie operacji biznesowych było by z góry narażone na poważne błędy.

Do budowania queries (read models) używa się projekcji, a więc procesów (lub nie procesów, zależnie od technologii) nasłuchujących danych eventów i na podstawie tych eventów budujących read model.Wyobraź sobie że chcesz zbudowań stan konta- do tego będziesz nasłuchiwał każdego eventu związane z kontem. Przy pierwszym evencie (np. AccountOpened/AccountCreated) utworzysz nowy rekord konta w bazie (zakładając że zapisujesz read modele to bazy właśnie) a następnie aktualizował ten rekord przy każdym kolejnym evencie dla tego konkretnego konta.

Jeśli chodzi o ładowanie stanu agregatów z eventów, to oczywiście może być tak że masz tysiące eventów i ładownie ich przy każdej obsłudze command może przynieść problemy wydajnościowe. By temu zapobiec stosuje się snapshots, ale nie należy tego mylić z tym czym są queries. Ponadto przy mniejszej ilości eventów składających się na agregat ładowanie ich za każdym razem nie jest tak naprawdę problemem przy dzisiejszych technologiach. W związku z tym snapshots należy raczej stosować wybiórczo, tam gdzie naprawdę mogą przynieść korzyści.

No i teraz, jak skladamy coś takiego do EventStore to jak jednoznacznie pokazywać gdy pobieramy to z tego eventstora, że dla danego User'a zmieniło się w tym evencie właśnie to pole i to z taką wartością? W dodatku co sprawdza się najlepiej jako EventStore? NoSQL, SQL, Key Value, czy może jakaś Kafka?

Twój model agregatu jest błędny, bo metody które (jak mniemam) mają obsłużyć commands przyjmują eventy. One powinny zamiast tego przyjmować albo parametry albo właśnie obiekt command, np:

User changeEmail(ChangeEmail changeEmail) { //To jest command
     if (...) { // Sprawdzenie jakiś zasad biznesowych
        // Ewentualny błąd jeśli jakieś wymogi nie są spełnione
     }

     this.email = emailChanged.getEmail();
     eventPublisher.publish(new EmailChanged(...));// Opublikowanie eventu- na tym etapie agregat sygnalizuje że wszystko się zgadza i email został zmieniony
     return this;
 }

Poza tym event powinien oczywiście posiadać powiązane IDs jak i wartości, np. w przypadku powyżej nowy email.

Tutaj kilka linków do artykułów, mam nadzieję że pomocnych:
Implementing an Event Sourced Aggregate
DDD – The aggregate
Event Sourcing (ogólnie)
Event Sourcing: Projections

Tutaj jeszcze link do mojej wypowiedzi na forum na temat czym tak naprawdę są agregaty. Temat dotyczył DDD ale sama idea agregatów dotyczy zarówno DDD jak i event sourcingu.

Obecnie pracuję nad własną implementacją event store w oparciu o bazę NoSQL. Może za jakiś czas wrzucę to na GitHub (obecnie mam w prywatnym repo) co by zaprezentować przykładową implementację jak i używanie event sourcingu z agregatami.

0

Fajnie wyjaśnione ale mam jedno pytanie. Mając coś takiego:

User changeEmail(ChangeEmail changeEmail) { //To jest command
     if (...) { // Sprawdzenie jakiś zasad biznesowych
        // Ewentualny błąd jeśli jakieś wymogi nie są spełnione
     }

     this.email = emailChanged.getEmail();
     eventPublisher.publish(new EmailChanged(...));// Opublikowanie eventu- na tym etapie agregat sygnalizuje że wszystko się zgadza i email został zmieniony
     return this;
 }

Rozumiem, że publikujemy event tylko jak wszelka walidacja itp. zostanie zakończona sukcesem? Co odnośnie faili? Wtedy nie ma sensu zapisywania eventu w event storze bo nie zmienił się stan obiektu, tak?

1

@Skoq: Dokładnie, event zapisujemy tylko wtedy gdy wszystko przejdzie pomyślnie. To jakby skutek pomyślnie użytego Command.

@Aventus @mad_penguin wiele mi to nakreśliło, zaraz zabieram się za te linki które podesłałeś i spróbuję sobie jakiś nawet głupi "sklep" w takim podejściu zakodzić.

0

Taki event sourcing czy cqrs to wycinek znanego i lubianego mvcc tyle, że bez cc :-) Ogólnie w trakcie nauki czy też wyboru jaki się dokonujesz warto też spojrzeć na to co właśnie tracisz.

Docelowo eventy jakie wpadają są zapisywane, ich się nie wycofuje, one są źródłem prawdy, co nie? To podejście oddaje programistom większą kontrolę nad tym co robią, ale w praktyce jest to bardzo uciążliwe wymaganie do spełnienia z poziomu samej aplikacji, która wykonuje też biznesową logikę - to tak jakby zadania w obrębie SQL sprowadzać do jednego spójnego zapytania / insertu.

W społeczności clojure znanym przykłademem event sourcingu / cqrs jest baza datomic - ale to jest to baza obsługująca transakcje zgodne z ACID, robiona przez łebskich ludzi, a nie przypadkowych średniaków. Natomiast bez transakcji taki event sourcing czy cqrs gwarantuje rozjazd w spójności danych. Wtedy mam czyste wątpliwości na ile źródło prawdy jest prawdą :-)

0

Mam jeszcze też pytanie co do czystego CQRS bez ES, wtedy to wyglądałoby tak:

User changeEmail(ChangeEmail changeEmail) { //To jest command
     if (...) { // Sprawdzenie jakiś zasad biznesowych
        // Ewentualny błąd jeśli jakieś wymogi nie są spełnione
     }

     this.email = emailChanged.getEmail();
     //bez wypuszczania eventu
     return this;
 }

Następnie takiego User'a rzucamy do jakiegoś CommandHandlera, który zapisze nam go do repo?

4

@Skoq:

Rozumiem, że publikujemy event tylko jak wszelka walidacja itp. zostanie zakończona sukcesem? Co odnośnie faili? Wtedy nie ma sensu zapisywania eventu w event storze bo nie zmienił się stan obiektu, tak?

To już zależy od tego jak wygląda Twój system, czy obsługujesz command synchronicznie czy asynchronicznie (np. jakiś worker nasłuchujący na kolejce). W pierwszym przypadku możesz po prostu rzucić wyjątkiem/zwrócić wynik. W tym drugim raczej nie obejdzie się bez eventu z failem (np. ChangingUserEmailFailed chyba że nie masz potrzeby nigdzie zwracać wyniku operacji (co mało prawdopodobne).

@semicolon

ale to jest to baza obsługująca transakcje zgodne z ACID, robiona przez łebskich ludzi, a nie przypadkowych średniaków. Natomiast bez transakcji taki event sourcing czy cqrs gwarantuje rozjazd w spójności danych. Wtedy mam czyste wątpliwości na ile źródło prawdy jest prawdą

Tyle że w przypadku CQRS (a co za tym idzie często i event sourcingu, bo te zagadnienia prawie zawsze idą w parze) to się nazywa właśnie eventual consistency. Zamiast z tym walczyć po prostu przyjmuje się to za naturalny element systemu, tym bardziej że bardzo często nie jest to przeszkodą wbrew temu co mogło by się wydawać na pierwszy rzut oka. To co ważne to transakcyjność po stronie zapisów (Commands), ponieważ w event sourcingu jak sama nazwa wskazuje to właśnie eventy są the only source of truth.

Oczywiście prawdą jest że wybierając takie a nie inne podejście coś zyskujemy, a coś tracimy. Tak jak jest ze wszystkim innym w wyborze architektury/wzorców.

@lavoholic

Następnie takiego User'a rzucamy do jakiegoś CommandHandlera, który zapisze nam go do repo?

Mi ogólnie ciężko wyobrazić sobie "czysty" CQRS bez eventów w innej postaci niż przy wykorzystaniu wzorca mediator i wykonywaniu wszystkich operacji w pamięci (procesie). Ale nawet i przy wykorzystaniu mediatora, nic nie stoi na przeszkodzie abyś nadal publikował eventy- różnica będzie polegała na tym że nie będziesz ich używał do budowania stanu obiektu (encji), a jedynie widoków (queries). To taka forma materialized view, gdzie ten view to właśnie strona z Query z CQRS.

0

Muszę przyznać, że teraz trochę nie czaję. Czym się różnią te dwie sytuacje - budowanie stanu oraz użycie eventów do query.

4

Czym się różnią te dwie sytuacje - budowanie stanu oraz użycie eventów do query.

Wyobraź sobie że dostajesz command aby zmienić email użytkownika. Na tym etapie ładujesz agregat (User) przy użyciu eventów, ponieważ jak już wcześniej wspomniałem jest to Twój only source of truth. Kiedy użytkownik zostanie zaktualizowany (opublikowany event), obsługujesz ten event w projekcji i aktualizujesz widok (Query side) w bazie danych czy gdziekolwiek to masz. Teraz wyobraź sobie że wchodzisz na stronę zarządzania użytkownikami- wszyscy użytkownicy którzy muszą zostać wyświetleni są ładowani z bazy widoku (Query). To nie jest ten sam obiekt User którego używasz to zapisów (commands). To jest wyspecjalizowany widok użytkownika, który może posiadać dodatkowe atrybuty, np. ostatnie 3 zamówienia online które nie są elementem agregatu User. Co za tym idzie, możesz budować widok użytkownika z eventów pochodzących z wielu różnych źródeł. Kiedy znowu chcesz zmienić email konkretnego użytkownika, wysyłasz do backendu jego ID i nowy email, i znów ładujesz agregat użytkownika za pomocą eventów. I tak dalej...

1

Tu nie chodzi o stan, ani o query - a o potencjalną możliwość jaka wynika z odpytywania o historię.

To co daje Ci ES to tylko fakt, że nie gubisz zmian. Baza PostgreSQL je gubi, ona za każdym razem je gubi, gdy coś nadpisuje.

Po to jest podział, abyś mógł wrócić do dowolnego miejsca w czasie - to jest wartość biznesowa, móc odtwarzać i analizować zdarzenia.

Ludzie tak robią, bo przestrzeń dyskowa teraz jest tania. Natomiast reszta tematów/pojęć, od których rozpoczynasz naukę ES to tylko skutek uboczny.

Eventy same w sobie są nieużyteczne, ich jest za wiele byś mógł w rozsądnym czasie udzielić odpowiedzi na podstawowe pytania, także po to budujesz stan, który w pewnym stopniu pełni analogiczną rolę co indeks w typowej bazie. Łapiesz?

0

Już widzę, trudno przestawić głowę z myślenia tabelkowego z RDBMS na myślenie gdzie żyje się bardziej obiektowo niż strukturalnie. Tak samo wydzielanie Bounded Contextow. Gdzie do tej pory User był wszystkim, a teraz jest oddzielnie Payerem, Customerem, Subjectem czy Receiverem. Nie wiem kiedy mi się to uda.. :D Musiałbym chyba być najpierw częścią takiego systemu żeby samemu go jakoś stworzyć, szczególnie z takim expem..

@Aventus: ejszcze mam też pytanie: zakładając, że mamy ten Read Model w postaci jakieś zwykłej bazy relacyjnej. Czyli przy Command zapisuję mój event to Event Store + zapis do Read Modelu, prawda? Wtedy np. mając takie coś:

class Order {
     UUID orderId;
     List<UUID> items;
     UUID customer;
     Price totalPrice;
     
     Result<OrderPlacingFailed, Order> placeOrder(OrderPlaced orderPlaced) {
          if (validation) {
              return Result.failure(OrderPlacingFailed);
          }
          this.items = orderPlaced.getItems();
          this.customer = orderPlaced.getCustomer();
          return Result.success(this);
     }    
}

Tutaj rozumiem, że Order powinien być moim agregatem, prawda? Więc powinien on posiadać w sobie List<UUID> items czy List<Item> items? Tak abym mógł tutaj obliczyć totalPrice? Czy taki agregat powinien mieć wtedy zależności do mojego Repo od Read Modelu oraz EventPublishera? Czy powinienem mieć jakiś handler, który by to wszystko robił? Natomiast wtedy trochę tracę tą "logikę" w moich encjach i stają one się dość anemiczne.
Ba czy Order nie powinien mieć też w sobie w jakim jest stanie, jaka będzie metoda zapłaty, sposób dostawy?

1

Ba czy Order nie powinien mieć też w sobie w jakim jest stanie, jaka będzie metoda zapłaty, sposób dostawy?

Zakładając, że masz wydzieloną domenę tworzenia zamówień to wydaje mi się równie dobrym pomysłem wydzielenie domeny odpowiedzialnej za wysyłkę (np. w zależności czy Twój totalPrice jest większy od X zł to wysyłka jest tańsza/darmowa) i płatności. W takim wypadku Twój dto wychodzący z tej pierwszej domeny nie miałby wiedzy o metodzie zapłaty czy sposobie dostawy. Ale to tylko przemyślenia raczkującego programisty :D

0

Racja, czyli w zasadzie totalPrice z Order powinien delegować jakąś komendę do modułu Payment, a następnie Shipment, nie?

3

@lavoholic: agregaty powinny ograniczać się do pewnego określonego zestawu powiązanych operacji. Czasem na agregat należy patrzyć jak na na maszynę stanu opisującą pewien proces. Co do Twojego przykładu, to przede wszystkim znów wygląda on błędnie bo znów przyjmuje event zamiast command. Co gorsza, zwraca konkretny wynik (fail)- a co z sukcesem? Chyba że ja nie do końca rozumiem co tam próbujesz osiągnąć.

Ogólnie zasada wywodząca się z DDD jest taka że agregaty mogą posiadać tylko ID innych agregatów, a nie referencje do obiektów. W przeciwnym razie przekraczamy granice transkcyjności jednego agregatu. Twój przykład zamówień jest dosyć kanonicznym zagadnieniem, i zazwyczaj rozwiązanie przedstawia się tak że masz swój Order który ma kolekcję OrderItem(s). Natomiast każdy OrderItem nie jest prawdziwym agregatem (tym jest właśnie Twój Item) a jedynie value objectem który posiada właśnie ID itemu jak i dodatkowe atrybuty- np. cenę pojedynczego itemu, jego ilość (w zamówieniu, nie ilość w magazynie) itp. No bo np. w przyszłości cena itemu może się zmienić, ale nie zmieni to faktu że to konkretne zamówienie ma order item z inną ceną- starą, albo ze zniżką itp.

Czy taki agregat powinien mieć wtedy zależności do mojego Repo od Read Modelu oraz EventPublishera?

Znów, tu nie powinno być żadnej zależności agregatu od read modelu. Jeśli np. masz wymóg (całkiem słuszny) by sprawdzić czy cena każdego order item zgadza się zanim dodajesz go do agregatu, to wykracza już to poza granicę transakcyjności tego agregatu. I tu znów z pomocą przychodzi DDD, a konkretnie domain services. Jest to więc serwis (handler) który przed wysłaniem command do agregatu sprawdzi pewne zasady biznesowe jak i wyciągnie potrzebne dane. Alternatywą byłoby wstrzykiwanie niektórych zależności bezpośrednio do agregatu, ale tu sam musisz sobie odpowiedzieć co wybrać. Ogólnie odradza się wstrzykiwania zależności do agregatu, szczególnie wstrzykiwania read-modeli, ale ja zawsze twierdzę że zasad nie należy się sztywno trzymać jeśli ich nagięcie ma sens dla rozwiązania konkretnego problemu.

Jak słusznie zauważyłeś może to prowadzić do anemicznych modeli, ale tam gdzie jest sens to stosować należy to robić. Innym przykładem gdzie zastosować serwis domenowy i obsługę operacji która wykracza poza transakcyjność jednego agregatu jest zapewnienie unikalności adresu email przy tworzeniu nowego użytkownika.

0

No i kto znowu właśnie widzę jak moje myślenie "relacyjne" gubi, a można to tak właśnie łatwo obskoczyć jak mówisz (zaplanowanie Order), natomiast co do komunikacji między modułami to racja - wystarczy wystawić event na który zareagują inne moduły. Jednak w takim wypadku jeśli mamy Order i powinien on opublikować Event na który zareagują inne moduły (no np. ten Payment) to jednak skądś musimy te informacje na temat tego Paymentu wziąć? Czy ten event je przekaże? Natomiast wtedy chyba wybrnąłby za swoją granicę (?). Bo jednak zapalnikiem do tego wszystkiego jest w zasadzie tworzenie Orderu.

Czym w zasadzie różni się Event od Commandu? Z perspektywy teoretycznej wiem, Command jest zapalnikiem natomiast Event jakby jego pozytywnym skutkiem. Natomiast od strony kodu czym byłaby ta różnica na przykładzie chociażby składania zamówienia?

Co do tego modelu to tam na końcu jest return Result.success(this);

1

@lavoholic: Kup sobie tą książkę: https://pragprog.com/book/egmicro/practical-microservices

Ona Cię krok po kroku wprowadzi w temat. Ja tej książki nie czytałem poza próbkami więc ciężko powiedzieć czy warto. Natomiast przekaz książki zrozumiałem czytając kod źródłowy. To nie było coś szczególnie pochłaniającego.

3

Co do tego modelu to tam na końcu jest return Result.success(this);

No właśnie na końcu jest że zwracasz success, a metoda zwraca Result<OrderPlacingFailed, Order>. Tego właśnie nie rozumiem.

Czym w zasadzie różni się Event od Commandu? Z perspektywy teoretycznej wiem, Command jest zapalnikiem natomiast Event jakby jego pozytywnym skutkiem. Natomiast od strony kodu czym byłaby ta różnica na przykładzie chociażby składania zamówienia?

Dodając do tego co pisałem wcześniej- klient (przeglądarka) wysyła polecenie dodania przedmiotu do zamówienia. Backend obsługuje request HTTP i zamienia go w command które jest wysłane dalej do warstwy domenowej. Tutaj command zostaje obsłużony przez agregat (lub najpierw serwis domenowy jak wspomniałem wyżej) i może się powieść lub nie. Powiedzmy że każde zamówienie może mieć maksymalnie 10 rożnych przedmiotów. Przy próbie dodania jedenastego, agregat odrzuci polecenie (command) ponieważ złamana zostanie zasada biznesowa. Może to skutkować eventem lub nie, zależnie od przyjętego podejścia. Jeśli operacja się powiedzie to agregat wyemituje event o dodanym przedmiocie zamówienia (OrderItemAdded).

Jednak w takim wypadku jeśli mamy Order i powinien on opublikować Event na który zareagują inne moduły (no np. ten Payment) to jednak skądś musimy te informacje na temat tego Paymentu wziąć? Czy ten event je przekaże? Natomiast wtedy chyba wybrnąłby za swoją granicę (?). Bo jednak zapalnikiem do tego wszystkiego jest w zasadzie tworzenie Orderu.

Tutaj trzeba odwrócić zależność- event emitowany przez Order nie musi wiedzieć nic na temat Paymentu. To Payment reagując na event emitowany przez Order (ale z jakiego agregatu event pochodzi Payment wiedzieć nie musi ani nie powinien!) odpowiednio zareaguje. Znów, zależnie od przyjętego podejścia event (np. OrderConfirmed) może zawierać TotaPrice lub Payment sam sobie obliczy całą cenę na podstawie wszystkich przedmiotów zamówienia (OrderItems), ich ilości oraz ceny w momencie składania zamówienia. Innymi słowy, proces agregatu Order kończy się na tym kiedy użytkownik zasygnalizuje chęć zakończenia zamówienia, a więc nie będzie więcej dodawanych przedmiotów. Mało tego- Order wcale nie musi być odpowiedzialny za sygnalizowanie takich rzeczy! Od tego możesz mieć w ogóle oddzielny agregat, np. OrderCompletion.

0

Hm, no tak takie rzeczy jak TotalPrice to rozumiem. Natomiast co z taką rzeczą jak PaymentMethod? Takie coś w zasadzie już następuje PO złożeniu zamówienia (czyli momencie kiedy użytkownik już niczego nie dodaje). Czy ogólnie np. składanie zamówienia to będzie jeden request, a wybieranie adresu dostawy oraz metody płatności to będzie już drugie zapytanie? Bo właściwie jeśli byłaby to jedność to trudno byłoby określić jak Payment ma wybrać metodę płatności, a Shipment metodę dostawy, prawda?

Ogólnie dzięki za poświęcony czas, że chce Ci się tak mi to tłumaczyć jak głupkowi.. :D

0

@Aventus

  1. Czy masz może jakieś dobre przykłady jak powinien wyglądać schemat takich eventów?
  2. W jaki sposób powinienem te eventy zapisywać w bazie danych? Jak rozumiem muszę mieć id aktualnego eventu, id poprzedniego eventu i event, czy coś jeszcze?
  3. W jaki sposób obsługiwać potencjalne duplikaty eventów? Czy powinienem cache'ować ostatnie eventy, żeby nie wykonać ich ponownie?
  4. Czy tylko jedna usługa powinna mieć możliwość edycji event logu i głównego źródła prawdy? W innym przypadku chyba trzeba wprowadzić rozproszone transakcje, czy się mylę?
  5. Czy masz może dobrą literaturę na ten temat oprócz wcześniej przytoczonego "Practical Microservices"?
3

Hm, no tak takie rzeczy jak TotalPrice to rozumiem. Natomiast co z taką rzeczą jak PaymentMethod? Takie coś w zasadzie już następuje PO złożeniu zamówienia (czyli momencie kiedy użytkownik już niczego nie dodaje). Czy ogólnie np. składanie zamówienia to będzie jeden request, a wybieranie adresu dostawy oraz metody płatności to będzie już drugie zapytanie?

Ah już rozumiem na czym polega Twoje zmieszanie. Tak, jak najbardziej! Wszystko złożone na oddzielne requesty. Tak jak masz problem ze zmianą paradgymatu związanego RDBMS na ES, tak tutaj nadal myślisz w kategoriach jakichś bardziej złożonych CRUDów. Podczas kiedy przy zastosowaniu ES i CQRS bardziej należy stosować rozwiązania typu task-based UI. Tylko że raz jeszcze zaznaczam- wcale nie musi (ale może) być tak że to Order inicjuje dalszy proces poprzez wyemitowanie eventu typu OrderCompleted. Możesz zastosowac odwrotne podejście- w czasie kiedy zamówienie jest aktualizowane (OrderItemAdded, OrderItemRemoved) inner serwisy- np. Payment- buduje swoje dane z tych eventów. Następnie kiedy nadchodzi czas kończenia zamówienia request może być np. wysłany bezpośrednio do Payment. Wtedy to np. agregat Payment może wyemitować event, który będzie przechwycony w serwisie zamówień i "zmapowany" na command, np. CompleteOrder. Wtedy agreat wyemituje event OrderCompleted już po zakończeniu płatności, i zaktualizuje swój stan np. po to by dokładnie to samo zamówienie nie mogło zostać znów zrobione.

Bo właściwie jeśli byłaby to jedność to trudno byłoby określić jak Payment ma wybrać metodę płatności, a Shipment metodę dostawy, prawda?

Dokładnie tak. Tym bardziej że przy zastosowaniu mikroserwisów możesz w ogóle mieć oddzielne serwisy API.

Ogólnie dzięki za poświęcony czas, że chce Ci się tak mi to tłumaczyć jak głupkowi.. :D

Nie ma sprawy, sam kiedyś miałem podobne pytania ;)

4
twoj_stary_pijany napisał(a):

@Aventus

  1. Czy masz może jakieś dobre przykłady jak powinien wyglądać schemat takich eventów?

Nie jestem pewny czy rozumiem o co Ci chodzi. Ogólnie to jestem zwolennikiem "czystych" domenowych eventów, a więc payload eventu powinien mieć tylko wartości domenowe. Np. OrderCreated mógłby wyglądać tak w naszej wyimaginowanej domenie:

{
  "OrderId": "guid",
  "UserId": "guid",
  "OrderTimeout": "timestamp",
  "MaxOrderItems": "10"
}
  1. W jaki sposób powinienem te eventy zapisywać w bazie danych? Jak rozumiem muszę mieć id aktualnego eventu, id poprzedniego eventu i event, czy coś jeszcze?

Tu znów- jestem zwolennikiem tego aby wszelkie dodatkowe atrybuty należały do metadanych eventu, "doklejane" przez infrastrukturę obsługującą eventy. To co będziesz miał w takich metadanych to już całkowicie od Ciebie zależy. Poprzedni event to tzw. causation ID. Chociaż ważniejsze (można polemizować) niż causation ID jest correlation ID, czyli to samo ID dla konkretnego procesu wiążące eventy razem. Np. od momentu kliknięcia "Complete Order" to zakończenia zamówienia wszystkie eventy będą miały to samo correlation ID. Zakładając że masz tabelę Events, taki rekord może posiadać np. kolumny EventId, EventName, Timestamp, Payload, CorrelationId. Oczywiście jest to tylko przykład. Sam przy mojej obecnej implementacji event store ograniczam metadane do minimum, ale może to ulec zmianie.

  1. W jaki sposób obsługiwać potencjalne duplikaty eventów? Czy powinienem cache'ować ostatnie eventy, żeby nie wykonać ich ponownie?

Nie ma co do tego jednej odpowiedzi. Tu można zdać się np. na brokera (jeśli ma możliwość de-duplikacji) lub właśnie trzymać rekord przetworzononych eventów, Zależnie od natury systemu można czyścić takie dane, np. ID eventów starszych niż X dni jeśli wierzymy że tak starte eventy nie mają szans znów zostać wysłane. Eventualnie należy stosować idempotence przy przetwarzaniu eventów, i po prostu liczyć się z tym że mogą trafić się duplikaty. Z własnego doświadczenia mogę jedynie powiedzieć że łatwiej to brzmi w teorii niż zrobić to dobrze w praktyce.

  1. Czy tylko jedna usługa powinna mieć możliwość edycji event logu i głównego źródła prawdy? W innym przypadku chyba trzeba wprowadzić rozproszone transakcje, czy się mylę?

Co rozumiesz przez edycję event logu? Log eventów jest "append only", czyli jedyne co można zrobić to dodać nowy event. Eventów nie usuwa ani nie edytuje się.

Ogólna zasada jest taka że rozproszone transakcje (w rozumieniu takim jak w bazach danych) to zło. Oczywiście znajdą się wyjątki gdzie inaczej nie da się tego załatwić. Natomiast w ogromnej większości przypadków stosuje się tzw. compensating events. Czyli jeśli zostaje wyemitowany event, ale dalej w procesie następuje coś co "unieważnia" poprzedni event. Np. mamy OrderCompleted a następnie płatność się nie udaje. W takim przypadku Payment emituje PaymentFailed, co dalej kończy się tym że agregat Order emituje OrderCancelled, OrderInvalidated lub coś podobnego. Ponadto często w bardziej złożonych, szczególnie rozproszonych procesach stosuje się sagas/process managers.

  1. Czy masz może dobrą literaturę na ten temat oprócz wcześniej przytoczonego "Practical Microservices"?

Ogólnie to proponuję zapoznać się z DDD, bo idzie mocno w parze z koncepcjami które wprowadza Event Sourcing. Np. takie agregaty ES zapożycza właśnie z DDD, tak samo jak pomysł emitowania eventów z agregatów do sygnalizowania zmian w reszcie systemu. Z książek mogę polecić Vaughn Vernon Implementing Domain-Driven Design. Ta książka ma również bardzo dobry aneks odnośnie event sourcingu, w tym przykłady implementacji własnego event store.

Tutaj świetna prezentacja "Not Just Events: Developing Asynchronous Microservices" odnośnie Twojego pytania co do transakcji.
Reference 4: A CQRS and ES Deep Dive z (darmowej) książki CQRS Journey. Ma to już trochę lat ale fundamentalne koncepcje pozostają bez zmian.

0

Jak to wszystko czytam to powątpiewam w siebie czy jest jakakolwiek szansa, że choć coś zbliżonego do tego jestem w stanie zaimplementować na jakimś ubogim systemie.. :D

0

@Aventus
ad 4. Źle się wyraziłem. Chodziło mi o to kto powinien dodawać rzeczy do event logu? Jak rozumiem można to zaimplementować np. na Kafce, że jakiś konsument nasłuchuje na eventy i je wszystkie zapisuje np. jako Avro/Parquet.

  1. Powiedzmy, że komenda "add_client" dodaje klienta i emituje event "add_client". Mam jeszcze jakiegoś workera, który nasłuchuje na event "add_client" i ten worker przygotowuje klientowi jakąś przestrzeń roboczą i emituje event "create_workspace". Teraz załóżmy, że straciłem swoją bazę danych i chciałbym ją odtworzyć. Wrzucam na kafkę event "add_client" oraz "create_workspace", a "add_client" podczas wykonywania dodatkowo emituje "create_workspace" przez co mój workspace utworzony dwa razy. Czy dobrze rozumiem, że taki event log powinien być przepuszczony jeszcze raz przez system, żeby odtworzyć bazę danych czy powinienem jakoś odsiać te zdarzenia pochodne?

Wielkie dzięki za dzielenie się wiedzą i za materiały.

3

@twoj_stary_pijany: nie to żebym się czepił ale odnośnie nazw tych eventów- eventy naprawdę powinny być w czasie przeszłym, commands powinny mieć wydźwięk imperatywny ;) Czyli eventy to ClientAdded, WorkspaceCreated itp.

Co do Twojego pytania- podany przez Ciebie przykład używa Kafki a ja nie mam z tym doświadczenia. Czy wyemitowanie eventu oznacza natychmiastowy zapis go do event store? Bo kiedy piszesz "konsument nasłuchuje" to mi na myśl od razu przychodzi coś co po (krótkim) czasie odbierze i zapisze event. No i już masz problem bo skąd gwarancja że między czasie jakiś agregat zostanie wczytany bez tego eventu? W tym scenariuszu masz eventual consistency na poziomie event store, a to nie wróży nic dobrego. Stąd lepszym podejściem jest bezpośredni zapis do event store, a następnie dopiero emitowanie eventów do kolejek- wymieniony wcześniej przeze mnie outbox pattern.

Tak więc odpowiedź na pytanie

.kto powinien dodawać rzeczy do event logu?

brzmi każdy. Każdy serwis przy emitowaniu eventu zapisuje go bezpośrednio do event store. Oczywiście implementację zapisu można wyabstrahować i dostarczyć w formie jakiejś biblioteki (NuGet, Maven itp).

Jeszcze raz to podkreślę- event store to "jedyne źródło prawdy w systemie", a więc de facto eventy muszą być zapisane transakcyjnie.

2

@twoj_stary_pijany uświadomiłem sobie że pominąłem bardzo istotny szczegół. Kiedy mowa o ES, eventach i agregatach to również mowa o event streams. Stream to nic innego jak zbiór eventów dla konkretnego agregatu- wtedy znając nazwę streamu (związaną z typem agregatu) oraz ID konkretnego agregatu wyciąga się wszystkie (lub ich część) eventy z takiego streamu. Co za tym idzie, metadane eventu o których pisałem wyżej muszą również zawierać co najmniej ID agregatu lub właśnie ID streamu. Często również stosowaną praktyką jest dopisywanie numeru sekwencyjnego eventu w obrębie danego streamu. Czyli stream dla Order 1 może mieć eventy z sekwencjami 1, 2, 3 a Order 2 eventy z sekwencjami 1, 2, 3, 4, 5.
To jak obsługiwać streamy zależy od użytych technologii i implementacji event store. Ja np. wykorzystuje fakt że baza NoSQL której używam jako event store ma rozbudowany mechanizm identyfikatorów z możliwością dodawania automatycznie generowanych sekwencji do końca takiego identyfikatoru. Co za tym idzie ID każdego eventu w event store zawiera jednocześnie dane streamu:

{nazwa agregatu}/{ID agregatu}/{sekwencja eventu}

np:

orders/af9b6c33-edab-4035-802b-bfa19af842a0/1
orders/af9b6c33-edab-4035-802b-bfa19af842a0/2
orders/af9b6c33-edab-4035-802b-bfa19af842a0/3
orders/af9b6c33-edab-4035-802b-bfa19af842a0/4

Pozwala to również na stosowanie optimistic concurrency jeśli sekwencje dodaje się "manualnie" w kodzie.

0

Zacząłem czytać Vernona bo miałem go od dawna w swojej bibliotece i muszę przyznać, że to chyba najgorsza książka jaką czytałem od czasów lektur szkolnych. Takiego lania wody i tekstów w stylu:

Już w następnym rozdziale dowiemy się czym jest kontekst ograniczony, już za chwilę, za momencik.

to dawno nie widziałem.

1 użytkowników online, w tym zalogowanych: 0, gości: 1