Event sourcing, CQRS, a do tego message brokers

0

Tak żeby potraktować ten temat jako skarbnicę pytań i odpowiedzi to mam pytanie:
Mam załóżmy agregat Order, pod niego mam jakiś OrderHandler - który odpowiada za publikowanie eventów, no i jakiś "recreate" agregatu na podstawie eventów z event store.
Teraz pytanie, bo mamy takowy OrderHandler, który wygląda tak:

@Repository
public class OrderHandler {
    private final MongoTemplate mongoTemplate;

    public OrderHandler(final MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    public void publish(final DomainEvent event) {
        mongoTemplate.insert(event, "events");
    }
}

No i załóżmy, że teraz przy evencie OrderPlaced ma iść e-mail do usera z potwierdzeniem zamówienia. Oczywiście mój EmailSender jest to całkowicie wydzielony moduł, uniwersalny dla pozostałych. Mam w planach wysyłać po prostu za pomocą Kafka Streams DTO z danymi e-maila do tego drugiego modułu - reagowałby on automatycznie przy evencie OrderPlaced generując jeszcze jakiś "event" typu EmailSent, który leciałby Kafką do innego modułu.
No i tutaj jak to ograć? Zostawić OrderHandler jako abstrakcje, która będzie mi odpowiadała tylko za kontakt z Event Store, a wyżej dodać abstrakcję, która wykona mi całą pozostałą magię, czyli coś na zasadzie:

class OrderPlacer {
   private final OrderHandler handler;
   private final ConfirmationEmailCreator creator;
  
  void placeOrder(PlaceOrder placeOrder) {
         orderHandler.handler(OrderPlaced.builder().///dane///
                                                     .build());
         confirmationEmailCreator.create(//tu przekaże dane, które będą potrzebne (adresat, treść itp.)); // i w środku wyślę jakimś Sourcem ten zbudowany wewnątrz DTO do modułu odp. za wysyłkę emaili?
  }
}
2

To by mijało się trochę z celem używania eventów (poza używaniem eventów dla samego event sourcingu oczywiście). Robiąc coś takiego co zaproponowałeś wiążesz logikę biznesową z działaniami które należy wykonać po tym jak zamówienie już zostanie złożone. Zamiast tego, po zapisaniu eventów do event store możesz je publikować- czy to za pomocą jakiegoś mediatora w pamięci czy też przy użyciu brokera- i wtedy mieć handlery nasłuchujące konkretnego eventu. Na przykład właśnie handler obsługujący wysłanie maila do klienta. Ale co ważne to fakt że będzie się to działo już bez "wiedzy" logiki biznesowej odpowiedzialnej za obsługę składania zamówienia. Na tej samej zasadzie możesz uruchamiać inne procesy biznesowe. Np. moduł odpowiedzialny za pobranie płatności reagujący na event że zamówienie zostało złożone, i pobierający płatność od klienta. Innymi słowy eventy pochodzącego z jednego agregatu mogą być odpowiedzialne za "uruchamianie" innych agregatów- jakiś inny handler przechwyci event i utworzy command wysłany do nowego agregatu.

Ciekawi mnie ten fragment handlera

    public void publish(final DomainEvent event) {
        mongoTemplate.insert(event, "events");
    }

Czy dobrze zgaduję że metoda publish jest wywoływana przez agregat, który przekazuje utworzony obiekt eventu do tej metody?

0

No dobrze, ale w takim razie muszę wyodrębnić jakoś, że tylko ten wybrany event będzie publikowany przez broker (chyba, że publikować wszystkie, a to nie będzie miało jakiegoś złego znaczenia z punktu widzenia wydajności/sensu).

Co do handlera to jest właśnie kolejne pytanie jak to w zasadzie powinno wyglądać (jakby cykl życia od requesta do response).
Dostaję strzał z API -> odbudowuję obecny stan(OrderHandler) -> wywołuję metodę reagującą na podaną komendę w tym agregacie (Order) -> wysyłam event do Store (jednocześnie publikując go jakimś b rokerem) -> co w zasadzie powinienem dostać przy wyjściu? - po prostu jakiś status bez body?

2
lavoholic napisał(a):

No dobrze, ale w takim razie muszę wyodrębnić jakoś, że tylko ten wybrany event będzie publikowany przez broker (chyba, że publikować wszystkie, a to nie będzie miało jakiegoś złego znaczenia z punktu widzenia wydajności/sensu).

Pracowałem przy systemach gdzie publikowane były wszystkie eventy. Brokery takie jak np. RabbitMQ są "mądre" i świetnie sobie radzą z obsługą wielu eventów. Tym bardziej że eventy nadaje się do wielu odbiorców, i subskrypcje można skonfigurować tak że odbierają tylko eventy którymi są zainteresowane.

Co nie znaczy że nie można publikować eventów selektywnie. Tyle tylko że to wymaga więcej pracy administracyjnej, ponieważ za każdym razem kiedy pojawia się jakiś moduł który potrzebuje danego eventu to trzeba skonfigurować ten event żeby był publikowany, jeśli nie był on już skonfigurowany wcześniej. Reasumując- wydajnością martw się wtedy kiedy pojawiają się problemy wydajnościowe.

Co do handlera to jest właśnie kolejne pytanie jak to w zasadzie powinno wyglądać (jakby cykl życia od requesta do response).
Dostaję strzał z API -> odbudowuję obecny stan(OrderHandler) -> wywołuję metodę reagującą na podaną komendę w tym agregacie (Order) -> wysyłam event do Store (jednocześnie publikując go jakimś brokerem) -> co w zasadzie powinienem dostać przy wyjściu? - po prostu jakiś status bez body?

To jest dobre pytanie. Zazwyczaj właśnie agregat nie powinien nic zwracać, a to z kolei wnosi ze sobą szereg innych zmian- np. asynchroniczność po stronie klienta. Kiedy klient wysyła request do API i dostaje odpowiedź OK, to ta odpowiedź oznacza tylko tyle że request został przyjęty i wysłany do przetwarzania. Wtedy np. w UI widać jakąś ikonkę ładowania, a logika frontendu oczekuje na otrzymanie wyniku asynchronicznie. W .Net można to bardzo łatwo osiągnąć używając SignalR które dostarcza dwukierunkowej komunikacji klient-serwer. Nie wiem jakie są odpowiedniki tego w innych technologiach, np. w ekosystemie Javy.

Ale zakładając że nie możesz albo nie chcesz użyć takiego rozwiązania, wtedy możesz po prostu zwrócić wynik z agregatu (chociaż co niektórzy puryści złapali by się za głowy). Oczywiście zakładając że logika agregatu wykonuje się synchronicznie, w tym samym kontekście co request HTTP. To rozwiązanie oczywiście nie będzie działać jeśli agregat wykonuje się w tym samym kontekście tylko jako pierwszy element procesu, a następnie gdzieś w innym serwisie inny agregat musi coś zrobić zanim cokolwiek zostanie wyświetlone dla użytkownika. Wtedy znów wracamy do tego co napisałem wyżej, a więc zwracanie wyników do klienta asynchronicznie.

0

Rozumiem, jednak kolejność jest poprawna? Czy zazwyczaj między to wchodzi coś jeszcze? Ciekawi mnie też fakt wykonywania logiki i walidacji w naszym agregacie (który ma nie być anemicznym modelem danych) - nasz agregat nie powinien mieć żadnych zależności do jakichś fasad itp., prawda? Co jeśli musimy zwalidowac coś co jest poza naszym agregatem? To znaczy, że coś źle zamodelowalem czy raczej takie sytuacje mają miejsce? Czy np. jeśli ktoś na moim Order dokonał już Payment to mój agregat Order powinien zareagować na ten event z zewnątrz i dokonać zmiany na agregacie zmieniając stan na OrderStare.PAID. Potem Shipment odpowiednio też zmienia OrderState na IN_DELIVERY, DELIVERED?

3

Rozumiem, jednak kolejność jest poprawna?

Tak, moim zdaniem wygląda ok.

Czy zazwyczaj między to wchodzi coś jeszcze?

O tym za chwilę.

Ciekawi mnie też fakt wykonywania logiki i walidacji w naszym agregacie (który ma nie być anemicznym modelem danych) - nasz agregat nie powinien mieć żadnych zależności do jakichś fasad itp., prawda? Co jeśli musimy zwalidowac coś co jest poza naszym agregatem? To znaczy, że coś źle zamodelowalem czy raczej takie sytuacje mają miejsce?

Oczywiście że takie sytuacje mają miejsce, i tak- nie powinno to należeć do odpowiedzialności agregatu. Użyję klasycznego przykładu bo ten temat nieustannie przewija się w pytaniach osób wchodzących w świat agregatów/DDD. Otóż mamy następujący problem:

Każdy nowy user musi mieć unikalny adres email. Zakładając istnienie agregatu User, jak zagwarantować unikalność adresu email rejestrując nowego użytkownika?

Często pierwsze co przychodzi na myśli to wstrzyknięcie jakiegoś serwisu/repozytorium do agregatu. Jest to podejście błędne, ponieważ pogwałca podstawową zasadę granicy transakcyjności pojedynczego agregatu. No bo czym jest instancja agregatu User? Jest użytkownikiem. A użytkownik nie wie, ani wiedzieć nie może o wszystkich adresach email zarezerwowanych w systemie. I tu przechodzimy do meritum- do tego służą serwisy domenowe. Posiadają one logikę biznesową która wykracza poza granice pojedynczego agregatu. Taki serwis możesz np. wstrzyknąć do handlera, i wywołać go zanim wywołasz metodę agregatu.

Czy np. jeśli ktoś na moim Order dokonał już Payment to mój agregat Order powinien zareagować na ten event z zewnątrz i dokonać zmiany na agregacie zmieniając stan na OrderStare.PAID. Potem Shipment odpowiednio też zmienia OrderState na IN_DELIVERY, DELIVERED?

To trochę inna sprawa. W tym przypadku faktycznie coś reaguje na event z zewnątrz, ale nie jest to agregat (agregaty reagują na commany a nie eventy) a np. event handler. Ten event handler utworzy odpowiednią command którą wyśle do agregatu, np. CompleteOrder który zakończy się wyemitowaniem eventu OrderCompleted. Tutaj ważna uwaga: w opisanym przypadku będziesz miał tzw. choreografię (ang. choreography), ponieważ poszczególne elementy systemu (event handlers) reagują na eventy w różnych miejscach. Jest to zdecentralizowane zarządzanie procesem. Alternatywą dla tego jest tzw. orkiestracja (ang. orchestration) gdzie masz centralne moduły zarządzania procesami, twz. process managers lub sagas. Wtedy taki centralny zarządca- np. OrderingProcessManager- reaguje na event i na ich podstawie tworzy commandy wysyłane do agregatów, aż do zakończenia procesu.

0

Poczytałem o sagach i muszę przyznać, że trochę rozwiązuje to kłopotów, podobnie serwisy domenowe - za bardzo nie wiedziałem gdzie w hierarchii mogę je umieścić.
Teraz przyszły mi kolejne zapewne banalne pytania :D
Jak wygląda w takim podejściu ogólnie pojęte Security? Zakładając, że wszystko jest oddzielnym modułem (czy też mikroserwisem) to zapewne Security będzie całkowicie wydzielonym mikroserwisem tak aby ukryć naszą "czarną stronę zabezpieczeń" gdzieś w oddzielnym module. Tylko, że! Zakładając, że używam Springa czy da się takie coś w ogóle uzyskać? Bo jednak jeśli chodzi o to to mam wrażenie, że część Security w Springu jest trudna do ujarzmienia w ten sposób - wymusza często sprawdzenie per endpoint, a żeby skutecznie wyciągnąć dane o naszym Subject, który się dobija pod dany endpoint - dajemy w Controllerach jakiegoś Principala żeby dostać gotowe dane ewentualnego usera - tak więc jednak wpychamy część "brudu security" w nasze moduły, które są stricte biznesowe. (tutaj myślę, że wiele mógłby wnieść również @Shalom ze względu na to, że Springa Security bardzo dobrze zna). Kolejna rzecz mamy też często wspólne rzeczy z punktu widzenia biznesu, czyli np. Customer, User, Receiver, Payer - to jest praktycznie ta sama osoba, jednak ze względu na nasze bounded contexty jest to całkowicie co innego. Jak takie coś połączyć? Skąd mamy wiedzieć, że ten Customer to jest ten User i ten Receiver?

1

To już podchodzi pod inne zagadnienie niż CQRS czy event sourcing więc nie ciągnął bym tematu w tym wątku. Ogólnie jeśli mowa o mikroserwisach to raczej ciężko się obejść bez użycia JWT. Ostatnio na ten temat było trochę w tych wątkach:

JWT, a bezpieczeństwo i trzymanie sesji
Zasada zabezpieczeń mikroserwisów

@lavoholic: wybacz, nie odpisałem wcześniej na Twoje drugie pytanie:

Kolejna rzecz mamy też często wspólne rzeczy z punktu widzenia biznesu, czyli np. Customer, User, Receiver, Payer - to jest praktycznie ta sama osoba, jednak ze względu na nasze bounded contexty jest to całkowicie co innego. Jak takie coś połączyć? Skąd mamy wiedzieć, że ten Customer to jest ten User i ten Receiver?

Dla tego w takich architekturach używa się globalnie unikalnych identyfikatorów (UUID) a nie np. intów. Tutaj małe sprostowanie- to nie jest tak że to całkowicie coś innego, tylko różne klasy różnie się do tego odwołują. To nadal ta sama osoba, a więc ID pozostaje to samo.

0

Cześć, pojawił się czas do powrotu do tematu, więc pojawiają się i pytania.
Z perspektywy takiego dodawania jakiegoś Item do Order przydałoby się walidować czy mamy na stanie Item o takim ID, w takiej ilości i czy jest w odpowiedniej cenie, prawda? Od usera powinniśmy dostawać coś takiego:

class AddItem {
  UUID orderId;
  UUID itemId;
  Quantity quantity;
}

No i musimy sprawdzić czy mamy taki agregat (po OrderId) - to się odbędzie po prostu przy odbudowywaniu stanu z eventów. No, ale dalej mamy do sprawdzenia czy Item, który user chce dodać jest na stanie w odpowiedniej ilości, czy w ogóle taki istnieje - no i pobrać skądś jego cenę. Jak takie dane powinniśmy chować? Bez sensu chyba przechowywać całą historię zmian dla każdego Item (czy wtedy nie musielibyśmy go też traktować jako agregat?). Nie lepiej trzymać tylko aktualne stany tych Item? Jeśli tak to też musielibyśmy mieć jakąś bazę z aktualnym stanem (np. SQL) i jednocześnie event store (jakiś NoSQL) - wtedy mamy dwa źródła danych w zasadzie przy jednym wejściu do (dodaniu produktu do zamówienia). Nie prosimy się wtedy o jakieś problemy? @Aventus

Podobna sytuacja jest np. przy dodawaniu kodu rabatowego do zamówienia. Czy jest sens trzymać jakiegoś Customer jako historię eventów? Szczególnie, że module order nasz customer będzie miał raczej ubogie dane, więcej informacji będzie o nim raczej jako user lub recipient.
Z góry dzięki.

1
lavoholic napisał(a)

Z perspektywy takiego dodawania jakiegoś Item do Order przydałoby się walidować czy mamy na stanie Item o takim ID, w takiej ilości i czy jest w odpowiedniej cenie, prawda?

To zależy od tego jak to chcesz zaimplementować. Owszem, możesz to sprawdzać w momencie dodawania Item do Order, ale możesz też sprawdzić czy wszystko się zgadza na sam koniec zamówienia. Jeśli chcesz to sprawdzać "w locie" to możesz zastosować serwis domenowy.

No i musimy sprawdzić czy mamy taki agregat (po OrderId) - to się odbędzie po prostu przy odbudowywaniu stanu z eventów. No, ale dalej mamy do sprawdzenia czy Item, który user chce dodać jest na stanie w odpowiedniej ilości, czy w ogóle taki istnieje - no i pobrać skądś jego cenę. Jak takie dane powinniśmy chować? Bez sensu chyba przechowywać całą historię zmian dla każdego Item (czy wtedy nie musielibyśmy go też traktować jako agregat?).

Musiałbyś, i całkiem możliwe że właśnie powinieneś.

Nie lepiej trzymać tylko aktualne stany tych Item? Jeśli tak to też musielibyśmy mieć jakąś bazę z aktualnym stanem (np. SQL) i jednocześnie event store (jakiś NoSQL) - wtedy mamy dwa źródła danych w zasadzie przy jednym wejściu do (dodaniu produktu do zamówienia). Nie prosimy się wtedy o jakieś problemy?

Tutaj znów wracamy do tego co- o ile dobrze pamiętam- już dyskutowaliśmy. Twój aktualny stan to tylko i wyłącznie agregaty które są ładowane za pomocą eventów. To co będziesz miał zapisane w bazie SQL (czy jakiejkolwiek innej) to tylko widok aktualnego stanu. Widok który jak już było wcześniej wspomniane może nie zawsze wskazywać najnowszy stan (eventual consistency),

Podobna sytuacja jest np. przy dodawaniu kodu rabatowego do zamówienia. Czy jest sens trzymać jakiegoś Customer jako historię eventów? Szczególnie, że module order nasz customer będzie miał raczej ubogie dane, więcej informacji będzie o nim raczej jako user lub recipient.

Customer może nie być najlepszym miejscem na to (chociaż niekoniecznie). Nic nie stoi jednak na przeszkodzie aby mieć lekki agregat od obsługiwania kodów rabatowych dla danego użytkownika, np. UserDisocuntsDetail. Ogólnie nie jest złą praktyką modelowanie "lekkich" agregatów obsługujących dany proces. Często jest to wręcz polecane. Preferuje się więcej mniejszych, wyspecjalizowanych agregatów, niż mało dużych które wiedzą wszystko.

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