Jak dobrze podzielić aplikację na moduły

0
ProgScibi napisał(a):

nobody01 przede wszystkim podejście zależy od tego czy to jest modularny monolit (i wtedy dane lecą często w RAMie przez jakiś EventBus) czy to są inne aplikacje (czyli moduł = aplikacja)

To fakt. W przypadku różnych aplikacji takie eventy z danymi mają większe uzasadnienie niż w przypadku systemu zintegrowanego "monolitu".
Popularnym przykładem są np. integracje systemów CRM z FK gdzie dane do faktury zwykle do systemu FK wysyła się kompletne czyli pełne dane klienta, pełne opisy produktów. Chociażby tylko dlatego, że temu drugiemu systemowi identyfikatory produktów czy kontrahentów z systemu pierwszego nic nie mówią, a bez sensu zadawać 2 pytania czyli
MESSAGE/EVENT Z CRM -> FK, że jest nowa FK
a następnie żądanie od FK do CRM o pełne dane kontrahenta i produktów. Czasem łatwiej będzie zrobić to wszystko w pierwszym kroku.

3
nobody01 napisał(a):

@somekind: W ogóle żadnych danych bys nie kopiował przez eventy?

Kopiowałbym w dwóch przypadkach: gdybym tego potrzebował, i gdyby mi za to płacili.

0

@somekind: Co to znaczy, że wtedy, gdy byś potrzebował? Mógłbyś podać jakiś przykład? W moim przypadku też pozornie potrzebuje, ale jednak każdy tu pisze, żeby nie kopiować.

@katakrowa: Czyli wysylalbys dane w eventach, gdy moduły są luźno powiązane ze soba? W Twoim przypadku wystarczy wysłać tylko jeden event z danymi z CRMa, aby FV mógł pracować bez odpytywania CRMa.

1

Załóżmy, że to co opisujesz jako moduły, to mikroserwisy systemu.

nobody01 napisał(a):

Moduł Ordering potrzebuje danych z modułu Products do jakiejś swojej logiki biznesowej (np. nie można dodać nieaktywnego produktu do zamówienia). Standardowym rozwiązaniem jest w takim przypadku emitowanie z modułu Products eventu zawierającego ProductId i IsActive. Moduł Ordering nasłuchuje na ten event, aktualizuje swoje dane i wszystko ładnie działa - nie musi odpytywać modułu Products, żeby sprawdzić, czy produkt jest aktywny.

Pytanie w czym przeszkadza ci odpytanie Products? Jeżeli jest to jedynie prosta informacja na temat stanu, to na ogół w niczym, a dołożenie do tego kolejnego miejsca w którym przechowywane są dane powoduje zwiększenie kosztów wytworzenia i utrzymania systemu. Musisz miec miejsce pod kolejną bazę danych, zajmować się jej serwisowaniem. Kolejka może ci paść i dane przestana przepływać. Można z tym walczyć, ale to znowu jest praca.

Po jakimś czasie pojawia się kolejne wymaganie: jeśli zamówienie zawiera produkt z grupy lub podgrupy X, wtedy zamówienie nie może zostać normalnie kontynuowane, tylko osoba Y musi dodatkowo zrobić Z. Żeby nie odpytywać modułu Products, moduł Ordering zawiera teraz dodatkowo hierarchię grup produktów aktualizowaną przez eventy z modułu Products.

O... właśnie. Podatność na rozwój spada wraz z wielkością systemu.

Czas mija i pojawia się kolejne wymaganie. Otóż produkt może mieć coś takiego jak dopłaty (np. przedłużenie jakiegoś tam kabla wchodzącego w skład produktu). W Ordering chcemy, żeby można było takie dopłaty wiązać z pozycjami zamówień. A czego potrzebujemy, by sprawdzić, czy daną dopłatę można powiązać z danym produktem? Oczywiście danych z modułu Products.

I prościej jest mieć ten moduł products z API, które może zostać rozszerzone wraz z rozwojem systemu, niż zastanawiać się, czy jak zmienisz strukturę payload w evencie to wszystkie konsumujące to usługi padną.

Już chyba wiadomo, do czego zmierzam: wraz ze wzrostem złożoności systemu moduły potrzebują coraz więcej swoich wewnętrznych danych. Możemy te dane kopiować, tylko czy ma to sens? Czy Ordering ma mieć wszystkie dane z Products skopiowane u siebie, żeby móc pełnić swoją funkcję? Co na ten temat sądzicie? Niezależność modułów to fikcja?

Czy to ma sens, musisz sam ocenić. Są przypadki kiedy to ma sens i takie gdzie będzie to absurdem, bo nie realizuje to żadnego wymagania, jest bardziej skomplikowane, podatne na awarie. Podałeś szkolny przykład, który pokazuje co można zrobić, ale niekoniecznie jest w tym sens.

Załóżmy, że mamy coś odrobinę bardziej z życia, jakiś sklep komputerowy, ma oddziały stacjonarne, sklep internetowy. Chcesz, żeby w sklepie internetowym była widoczna dostępność jakiegoś produktu. Magazyn to zbiór rekordów przyjęciach i wydaniach produktów. Wielu ludzi przegląda witrynę sklepu, ale zamawiają znacznie rzadziej. Czyli musiałbyś w tym wypadku, ileś razy na sekundę odpytywać magazyn o stany różnych produktów, a ten przeglądałby wszystkie rekordy w DB, żeby policzyć czy coś jest, czy NIE MA na magazynie. Nie możesz sobie tego cache'ować po stronie sklepu, bo to nie on odpowiada za przyjęcia towarów i część wydań (sklepy stacjonarne). Odpytywać tez nie możesz, bo event sourcing nie wyrobi przy takim ruchu. Ponieważ masz dużo większy ruch na czytaniu, niż na zmianach, potrzebujesz jedynie drobnej części informacji (stanu obecnego) a nie historii, możesz zastosować jakiś event bus do powiadamiania wszystkich zainteresowanych o miejscu gdzie coś się zmieniło, lub nowych wartościach.
Czyli:
Sklep ma własną, ulotną bazę danych (np. Redis), która ma listę produktów ze stanami. Buduje ją sobie przy starcie korzystając z danych z Magazynu (odpytuje przez jakieś API). W momencie zmiany stanu, Magazyn emituje sygnał zawierający id zmienionego produktu, typ zmiany (zmiana ceny, zwiększenie stanu, zmniejszenie stanu, sklep odbiera sygnał, jeżeli zmiana jest dla niego istotna (cena, zmniejszenie stanu) odpytuje magazyn o nowe wartości. Możesz też wrzucić do sygnału nowy stan produktu (cena, stan magazynowy itd.) i wtedy nie trzeba juz pytać, wystarczy przetworzyć stan. W praktyce tak czy inaczej, byłby to pewnie osobny serwis odpowiedzialny wyłącznie za przechowywanie aktualnego stanu produktów i obsługę sygnałów o zmianach, udostępniający te dane na żądanie ze sklepu.

1
nobody01 napisał(a):

@katakrowa: Czyli wysylalbys dane w eventach, gdy moduły są luźno powiązane ze soba? W Twoim przypadku wystarczy wysłać tylko jeden event z danymi z CRMa, aby FV mógł pracować bez odpytywania CRMa.

"Luźno" to pojęcie niejasne. Zrobiłbym tak gdyby:

  • Moduł FK nie miał innej możliwości by dostać się do bazy CRM ;
  • Była sytuacja, w której te dane do faktury są niezbędne w FV i sama informacja o zmianie jej stanu w CRM nic nie wnosi dla systemu FK ;

Niezależnie od powyższych rozważań zrobiłbym:

  • API do pobierania listy faktur ;
  • API do pobierania danych pojedynczej faktury ;
  • mechanizm callback/eventów dla systemów zewnętrznych informujący o pojawieniu się lub zmianie statusu faktury w systemie.

Taki zestaw narzędzi można potem używać w ramach własnego zintegrowanego systemu jak i podczas integracji z zewnętrznymi.

2

Obejrzyj

, odkrywanie granic nie jest trywialne, dużo zależy od Twojej domeny

2

Zostałem wywołany przez @nobody01 więc się stawiam. Tutaj jest kilka problemów i możliwych rozwiązań (co by nie było zbyt łatwo ;)).

Najpierw to o czym pisał katakrowa w pierwszym poście, a więc kwestia tego czy modułowość == oddzielne bazy. Po części się z nim zgadzam co do wątpliwości, ale nie zgadzam się z wnioskiem- mianowicie z tym że powinno się używać jednego "źródła prawdy" oraz nie należy robić "kopii bazy". O żadnej kopii całej bazy nie ma mowy, ale trzeba sobie również odpowiedzieć czy chcemy iść w bardziej pragmatyczne podejście czy też nie- co powinno zależeć od złożoności systemu.

Do tego dochodzi to czy przez moduły rozumiemy jeden proces ale logicznie rozbity na poszczególne moduły (modularny monolit), czy też system rozproszony (mikroserwisy).

Ja wypowiem się w kontekście systemu gdzie każdy moduł ma swoją bazę danych, a to czy jest to system rozproszony czy nie to już sprawa drugorzędna. Rozwiązania jakie na szybo przychodzą mi do głowy:

1. Pełna asynchroniczność i każdy moduł budujący dane których potrzebuje
W tym przypadku moduł Ordering nasłuchiwałby eventów z Products i tworzył własną kopię, wliczając w to dane z produktów. Oczywiście te dane nadal powinny być ograniczone, a więc zawierać tylko to co potrzebne do wyegzekwowania zasad biznesowych. Co ważne, kształt tych danych może być całkowicie inny niż w module z którego pochodzą. Czyli np. dla każdego produktu Ordering może przechowywać jeden dokument JSON (bazy NoSQL) który zawierał będzie wszystkie informacje jakie Ordering potrzebuje, w tym podgrupy, tagi, dostępne rozszerzenia itp.

Zalety

  • Proces zamówienia jest ograniczony do małej liczby modułów, ponieważ większość danych potrzebnych dla zasad biznesowych jest już dostępna
  • Duplikowanie i denormalizacja danych to dziś już żadna nowość, a w systemach rozproszonych wręcz standard (mimo wszystko umieszczę to również w wadach). Tworzymy różne widoki tych samych danych na konkretne potrzeby.
  • Proces jest wykonywany w ograniczonym kontekście (moduł Ordering) przez co jest łatwiej kontrolowany

    Wady

  • Denormalizacja danych (zaleta powyżej)
  • Eventual consistency
  • Potencjalne wyciekanie wiedzy domenowej z jednego modułu do innych (ale ryzyko również występuje przy innych podejściach, jak pokazane niżej)

2. Pełna asynchroniczność, każdy moduł ograniczający dostęp do swoich danych, zaangażowanie dłuższego procesu
Rozbijamy konkretny przypadek na większy proces i angażujemy w niego te moduły które potrzeba. W tym przypadku moduł Ordering zasygnalizowałby eventem że składanie zamówienia się zaczęło, ale to moduł Products odpowiedzialny by był za walidacje danych dotyczących produktu. Wynik tej walidacji były również eventem (sukces lub porażka) którego nasłuchiwałby moduł Ordering i na tej podstawie zamówienie zakończyło by się powodzeniem lub błędem. Mamy tutaj podejście event-driven gdzie zdarzenie w jednym module aktywuje zdarzenie w innym.

Zalety

  • Większość danych i zasad biznesowych ograniczone do konkretnych modułów
  • Brak eventual consistency- moduł Products będzie miał dostęp do aktualnych danych

    Wady

  • Nadal mamy ryzyko wycieku wiedzy domenowej tylko w odwrotnym kierunku- teraz to moduł Products musi wiedzieć i reagować na jakieś zewnętrzne procesy wymagające walidacji produktów
  • Musimy angażować pełnoprawny proces tam gdzie możemy mieć coś stosunkowo trywialnego
  • Koordynacja procesu- tutaj z pomocą może przyjść orkiestracja (ang. orchestration) ale nadal jest to dodatkowa warstwa skomplikowania

3. Synchroniczny proces, każdy moduł ograniczający dostęp do swoich danych
W tym przypadku poszczególnie serwisy odpytywane są za pomocą synchronicznej komunikacji (np. HTTP). Ordering może wysłać polecenie do Products w celu walidacji produktów, lub na odwrót- spytać Products o zwrócenie zasad i wykonanie walidacji po stronie Ordering.

Zalety

  • Większość danych i zasad biznesowych (to niekoniecznie) ograniczone do konkretnych modułów
  • W pełni łatwo kontrolowany proces
  • Brak eventual consistency

    Wady

  • Tight coupling między modułami- Ordering wie że musi odpytać Products o coś, w przypadku błędu requestu cała operacja nam siada
  • Nie unikamy wycieku wiedzy domenowej ze względu na punk powyżej
  • Ryzyko synchronicznego angażowania wielu modułów łańcuchem wywołań (moduł A odpytuje B, który z kolei odpytuje C, który odpytuje D...) który prowadzi w zasadzie do spaghetti na poziomie architektury

Dodatkowo do powyższych, spróbowałbym spojrzeć na problem "spoza pudełka". W pierwszej kolejności spytałbym czy egzekwowanie takich zasad jest w ogóle warte walidacji. Teoretycznie, klient nigdy nie powinien osiągnąć stanu w którym wysłałby np. request z produktami posiadającymi niepoprawne rozszerzenia. Jeśli do czegoś takiego by doszło to coś nam siadło już wcześniej, albo ktoś umyślnie powoduje błąd. Tak czy inaczej będziemy mieli błąd, na tym czy innym poziomie. Koniec końców będzie to wymagać interwencji supportu. Często bywa tak że lepiej przepuścić błędne zamówienie, i rozwiązań problem na poziomie biznesowym. Tak więc pojawiają się pytania natury biznesowej, a nie tylko technicznej.

Załóżmy jednak że faktycznie przepuszczenie czegoś takiego przez system nie może mieć miejsca. Wtedy ja proponuję jedno z powyższych rozwiązań. Problem wycieku wiedzy domenowej między Ordering a Products można częściowo rozwiązać wprowadzając dodatkowy moduł odpowiadający za szerszą walidację zasad. Wtedy ten moduł będzie posiadał potrzebne dane z Products, i reagował na to co dzieje się w Ordering. Owszem, ktoś może stwierdzić że i tutaj mamy przecież wyciek wiedzy domenowej- ale tym razem mamy od tego przeznaczony specjalny moduł, a nie moduły które "przypadkowo" muszą obsłużyć coś co do nich nie należy (nie jest w ich gestii odpowiedzialności).

4

Moim zdaniem, błędem jest patrzenie na projekt wyłącznie przez pryzmat tego jak łatwo będzie to zaprogramować i czy będzie to zgodne z jakimś tam losowo wybranym wzorcem. System ma realizować jakąś tam funkcjonalność biznesową, a nie startować w konkursie na miss jakiejś tam architektury. Wybór technologii, struktury, sposobu komunikacji pomiędzy modułami, sposobu przechowywania danych zależy od tego na co kładziemy większy nacisk i wiąże się zawsze z jakimiś kompromisami. Jak chcemy szybko pisać, to pewnie odbije się to na skalowalności i łatwości utrzymania systemu. Jak zależy nam na niezawodności, to pewnie spadnie dostępność itd.

Warto popatrzeć sobie na AWS well architectured framework i jego kryteria :

  • Operational excellency (system jest łatwy w rozwoju i robi to co ma robić)
  • Security - spójność danych, limitowanie dostępu itd.
  • Reliability - brak SPoF, backupy, monitoring
  • Performance - efektywne dobranie rozwiązań do potrzeb, czy wybrać SQL, czy NoSQL, kontenery, czy VM itd.
  • Cost optimization - czy to wszystko wypisane wyżej nie kosztuje niepotrzebnie dużo.

Postrzeganie systemu wyłącznie przez jeden z tych wskaźników jest błędem, bo możemy sobie np. zbudować łatwy i szybki w rozwoju system, ale co z tego jak trzeba będzie wyskalować infrastrukturę, do poziomu przy którym nie będzie nas stać na jej utrzymanie, albo wszystko fajnie, ale nie da się przypisać ról użytkownikom.

0

Na wstępie dziękuję za tak rozbudowane odpowiedzi, @piotrpo i sczególnie @Aventus :)

Ten fragment:

Ryzyko synchronicznego angażowania wielu modułów łańcuchem wywołań (moduł A odpytuje B, który z kolei odpytuje C, który odpytuje D...) który prowadzi w zasadzie do spaghetti na poziomie architektury

Właśnie czegoś takiego chciałbym uniknąć. Czy ma sens mieszanie podejść? Np. moduł Ordering ma kopię tych danych z modułu Products, których najczęściej potrzebuje, a te dane, które potrzebuje rzadziej, pobiera bezpośrednio z API modułu Products?

3

Ma sens, bo być może możesz sobie pozwolić na odczyty z cache (własnej kopii danych, która może nie być spójna w danym momencie), natomiast przy jakimś zapisie musisz mieć świeże dane, aby przeprowadzić walidację.

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