Modularny monolit - jak najlepiej ugryźć?

Modularny monolit - jak najlepiej ugryźć?
HO
  • Rejestracja:prawie 5 lat
  • Ostatnio:ponad rok
  • Postów:19
0

Cześć,

wyobrażam sobie aplikację podzieloną w ten sposób

Kopiuj
|
|--- module 1
    |
    |- Configuration1.java
|
|--- module 2
    |
    |- Configuration2.java
|
|--- mainModule
    |
    |- Application // tutaj jest main() i wywołanie SpringApplication.run
    |- application.yaml

Oczywiście w uproszczeniu, każdy moduł ma swój build.gradle, testy, kod źródłowy, resources.
Każdy moduł jest samodzielny i nie wie nic na temat świata poza nim. Tylko main module spina wszystko do kupy i uruchamia appkę.
Poprzez gradle wrzucam sobie submoduły do głównego modułu

Kopiuj
implementation(
            project(':module1'),
            project(':module2'),
)

I teraz tak - każdy submoduł ma swój scope i swoje beany. Nie powinny one być widoczne poza modułem (lub nie powinny być widoczne dla innych podmodułów). Każdy submoduł ma swoje serwisy, repozytoria, http kontrolery... W idealnym scenariuszu wyobrażam sobie to tak, że dołączam moduł i mam gotowy nowy feature w aplikacji.
Wiem, że mogę sobie normalnie utworzyć aplikację wielomodułową, ale wtedy mam osobny run aplikacji per moduł. Każdy moduł ma swój SpringApplication.run. W przyszłości możliwe, że będę szedł w mikroserwisy, na teraz jest na to za wcześnie. Potrzebuję dobrze ogarnięty modularny monolit w jednej paczce, jeśli będzie potrzeba to niedużym nakładem pracy podzielę appkę na zupełnie niezależne, osobne moduły.
Ale wracając - każdy submoduł ma swoje beany, np

Kopiuj
@Configuration
class Configuration1 {

    @Value("${property.from.main.module}")
    private String propertyFromMainModule;

    @Bean
    public Bean1 bean1() {
        // using propertyFromMainModule to create the bean
    }
    
}

problem jest taki, że propertisy z application.yaml są zdefiniowane w main module i nie przenikają one do submodułów. Nie da się ich także zdefiniować na poziomie submodułów (osobny application.yaml per moduł - nie ładuje i wyrzuca wyjątek, że nie znaleziono propertisa). Nie da się także zinicjalizować beanów na poziomie main module, bo nie są one widoczne w danym submodule, do którego bean należy.

Macie jakiś pomysł jak wstrzyknąć properties z application yaml do submodułów?
Albo może doradzicie jakiś inny, sprawdzony approach na modularny monolit wg opisanych założeń.

Dzięki!

edytowany 3x, ostatnio: hopsey
Charles_Ray
  • Rejestracja:około 17 lat
  • Ostatnio:dzień
  • Postów:1875
4
  1. Nie rozumiem dlaczego moduły nie mogą o sobie wiedzieć i potrzebują jakiejś koordynacji poprzez maina - to antypattern (wąskie gardło, będzie mnóstwo konfilktów, współdzielony kod, single point of failure)
  2. Możesz poczytać o hierarchii kontekstów w Springu - nie używałem. Jeśli chodzi o rozdzielenie propertiesów - nie wiem czy to w tym momencie nie jest wtórny problem, który wynika z użytego frameworka, a którego rozwiązanie niewiele daje
  3. Zamiast modułów Gradlowych możesz zrobić pakiety - ja migrowałem się właśnie z modułów na pakiety ze względu łatwości developmentu (za długo trwa wynoszenie wspólnych części do osobnego modułu)
  4. Możesz użyć ArchUnit do walidacji założeń architektonicznych

”Engineering is easy. People are hard.” Bill Coughran
edytowany 3x, ostatnio: Charles_Ray
superdurszlak
Zaintrygował mnie ten ArchUnit - jakie ma ciekawe możliwości? Z tego co widzę po przykładowych testach, to pokazali głównie "sanity checki" - konwencje, szukanie cykli, niewołanie jednego kontrolera z drugiego...
Charles_Ray
Np. takie, że z warstwy domeny nie można wołać niczego z infrastruktury. W zasadzie można tam sprawdzać dowolne konwencje przyjęte w projekcie.
danek
pakiety mają te wadę, że są płaskie. Nie da się ich sensownie zagnieżdżać. Dodatkowo w kotlinie i tak nie działają :(
TS
  • Rejestracja:prawie 5 lat
  • Ostatnio:ponad 4 lata
  • Postów:394
2

Do SpringApplication.run można przekazać wiele klas. Pojęcia nie mam jak to zadziała w praktyce, nigdy nie próbowałem, ale może jak tam przekażesz niewielką konfiguracją z main, która ładuje te propertiesy to będą one widoczne w poszczególnych submodułach?

Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

Jeśli każdy moduł ma swoje kontrolery HTTP to brzmi to bardziej jakbyś miał mikroserwisy a nie modularny monolit. Przecież możesz mieć jeden serwis API który będzie delegował pracę do konkretnego modułu, a więc kontrolery mogą być zdefiniowane w jednym miejscu. Aby to osiągnąć proponuję poczytać o wzrocu mediator. Moduły powinny być granicą konkretnej sub-domeny Twojej aplikacji, a więc przede wszystkim skupiać się na oddzieleniu logiki biznesowej. Infrastruktura to sprawa drugorzędna.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
Charles_Ray
Czyli gdzie byś trzymał kontrolery? Załóżmy, że modułów enkapsulujacych bounded conteksty jest 20.
Aventus
@Charles_Ray: Przy takiej złożoności to zastanowiłbym się właśnie nad mikroserwisami. W przeciwnym razie kontrolery trzymał bym tam gdzie powinny być, czyli w programie webowym wystawiającym API. Tutaj ważna uwaga- zakładam że mówimy o kontrolerach które służą do tego do czego powinny służyć, czyli jedynie oddelegowania poleceń dalej, do warstwy domenowej, oraz do wszystkiego co związane z obsługą HTTP. Żadnej logiki biznesowej itp.
Charles_Ray
A jaki problem rozwiążą tutaj mikroserwisy?
Aventus
Przy takiej ilości kontekstów masz z pewnością bardzo złożoną domenę (oczywiście zakładam że nie są to źle wydzielone konteksty). Zapewne każdy musi wystawić jakieś API, a więc idąc tym tokiem zapewne mamy różne wymagania co do skalowalności poszczególnych kontekstów- raczej mało prawdopodobne że każdy jest używany z taką samą intensywnością, i w taki sam sposób (jeden np. może obsługiwać interakcje użytkownika, ale inny już odpowiadać na jakieś zautomatyzowane procesy lub konfigurację wykonywana przez wewnętrzny personel). Przy takiej różnorodności...
Aventus
... potrzeba autonomii poszczególnych modułów aż się sama nasuwa na myśl. A takiej autonomii nie osiągniesz przy monolicie, tam gdzie masz deployment jednego artefaktu lub góra kilku, ale deployowoanych razem. Do tego podział na serwisy wymusi "fizycznie" odpowiednie rozdzielenie modułów, zarówno w sensie podziału domeny jak i na poziomie infrastruktury. Co za tym idzie łatwiej utrzymać samodyscyplinę między zespołami.
danek
  • Rejestracja:ponad 10 lat
  • Ostatnio:7 miesięcy
  • Lokalizacja:Poznań
  • Postów:797
2

U mnie obecnie wygląda to tak: jest jeden moduł na api restowe. Ma on zależność na wszystkie moduły, których potrzebuje. Klasy zazwyczaj widzi przez interface. W jednym module siedzi zazwyczaj jedna funkcjonalność, która korzysta z modułów, które komunikują się z np baża danych. I tak przykładowo dla prostej funkcjonalności, która na request odczytuje coś z bazy:
Moduł api: jest tutaj rest controller, widzi moduł domenowy
Moduł domenowy: tutaj jakaś logika, filtrowanie cokolwiek, widzi moduł do komunikacji z bazą.
Moduł bazy danych: bezpośredni odczyt danych i zamiana ich na jakiś sensowny format

Jakie to ma wady? Modele. Dużo modeli. Idealnie jakby każdy moduł miał swój zestaw modeli na jeden byt, ponieważ może wtedy sobie np dowolnie ucinać zbędne dany. Dla przykładu obiekt z bazy ma pola A,B,C. W domenie potrzebujesz tylko A i przemapować B na podstawie danych z innego miejsca. W api chcesz zwrócić tylko A w zależności od B. I tak na każdym etapie operujesz tylko na tych polach których faktycznie potrzebujesz. Niestety, bawisz się w mapowanie wszystkiego zawsze.

Przy okazji możesz się zainteresować architekturą hexagonalną, ponieważ ładnie do tego pasuje
https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/


Spring? Ja tam wole mieć kontrole nad kodem ᕙ(ꔢ)ᕗ
Haste - mała biblioteka do testów z czasem.
edytowany 2x, ostatnio: danek
HO
  • Rejestracja:prawie 5 lat
  • Ostatnio:ponad rok
  • Postów:19
0
Charles_Ray napisał(a):
  1. Nie rozumiem dlaczego moduły nie mogą o sobie wiedzieć i potrzebują jakiejś koordynacji poprzez maina - to antypattern (wąskie gardło, będzie mnóstwo konfilktów, współdzielony kod, single point of failure)
  2. Możesz poczytać o hierarchii kontekstów w Springu - nie używałem. Jeśli chodzi o rozdzielenie propertiesów - nie wiem czy to w tym momencie nie jest wtórny problem, który wynika z użytego frameworka, a którego rozwiązanie niewiele daje
  3. Zamiast modułów Gradlowych możesz zrobić pakiety - ja migrowałem się właśnie z modułów na pakiety ze względu łatwości developmentu (za długo trwa wynoszenie wspólnych części do osobnego modułu)
  4. Możesz użyć ArchUnit do walidacji założeń architektonicznych

To nie jest tak, że main koordynuje ich działanie. Main tylko bootstrapuje aplikację. Gdybym chciał pójść w mikroserwisy, mógłbym go całkowicie usunąć.
Też nie jest tak, że nic o sobie nie wiedzą. Są podzielone według domeny a komunikują się zdarzeniami. Moduł faktur wie o zdarzeniu order.paid modułu zamówień i samodzielnie wystawia fakturę, na koniec emitując odpowiednie zdarzenie.
Mam też moduł z częścią wspólną (interfejsy, commandbus, eventbus, itp), który jest widziany przez każdy moduł.
Tak jak napisałeś, skłaniam się do porzucenia modułów, rozwinięcia pakietów wewnątrz jednego modułu, co bardziej wpisuje się w monolit.

tsz napisał(a):

Do SpringApplication.run można przekazać wiele klas. Pojęcia nie mam jak to zadziała w praktyce, nigdy nie próbowałem, ale może jak tam przekażesz niewielką konfiguracją z main, która ładuje te propertiesy to będą one widoczne w poszczególnych submodułach?

Czyli każdy moduł ma swoją klasę ze @SpringBootApplication? Sprawdzę nawet z ciekawości jak do działa.

Aventus napisał(a):

Jeśli każdy moduł ma swoje kontrolery HTTP to brzmi to bardziej jakbyś miał mikroserwisy a nie modularny monolit. Przecież możesz mieć jeden serwis API który będzie delegował pracę do konkretnego modułu, a więc kontrolery mogą być zdefiniowane w jednym miejscu. Aby to osiągnąć proponuję poczytać o wzrocu mediator. Moduły powinny być granicą konkretnej sub-domeny Twojej aplikacji, a więc przede wszystkim skupiać się na oddzieleniu logiki biznesowej. Infrastruktura to sprawa drugorzędna.

Faktycznie, trochę bliżej do mikroserwisów. Plan jest taki, że kiedyś aplikacja zostanie podzielona na mikroserwisy, dlatego wszystko jest dosyć radykalnie odseparowane. Łatwiej będzie wydzielić moduł do pojedynczego mikroserwisu. Cała warstwa aplikacji (każdy punkt wejścia, w tym http) steruje domeną przez commandy. CommandBus dispatchuje komendę wywołując odpowiedni handler, ten dealuje już z agregatami, perzystuje zdarzenia (mam ES).
Moduły są podzielone według domeny. Jak napisałem wyżej, zrzucę to wszystko do pakietów, httpa wyrzucę poza moduły.

Charles_Ray
  • Rejestracja:około 17 lat
  • Ostatnio:dzień
  • Postów:1875
1

Odnośnie sposobie podziału na moduły i komunikacji między nimi wydaje mi się to OK. Pamiętaj tylko, że obecnie masz monolit, a wiec jeden JVM. Dodanie większej liczby adnotacji niewiele tutaj pomoże. Pójście w mikroserwisy nie zawsze ma sens i się opłaca - masz potrzebę niezależnego skalowania modułów lub wiele niezależnych zespołów?


”Engineering is easy. People are hard.” Bill Coughran
edytowany 1x, ostatnio: Charles_Ray
HO
  • Rejestracja:prawie 5 lat
  • Ostatnio:ponad rok
  • Postów:19
0
danek napisał(a):

U mnie obecnie wygląda to tak: jest jeden moduł na api restowe. Ma on zależność na wszystkie moduły, których potrzebuje. Klasy zazwyczaj widzi przez interface. W jednym module siedzi zazwyczaj jedna funkcjonalność, która korzysta z modułów, które komunikują się z np baża danych. I tak przykładowo dla prostej funkcjonalności, która na request odczytuje coś z bazy:
Moduł api: jest tutaj rest controller, widzi moduł domenowy
Moduł domenowy: tutaj jakaś logika, filtrowanie cokolwiek, widzi moduł do komunikacji z bazą.
Moduł bazy danych: bezpośredni odczyt danych i zamiana ich na jakiś sensowny format

Jakie to ma wady? Modele. Dużo modeli. Idealnie jakby każdy moduł miał swój zestaw modeli na jeden byt, ponieważ może wtedy sobie np dowolnie ucinać zbędne dany. Dla przykładu obiekt z bazy ma pola A,B,C. W domenie potrzebujesz tylko A i przemapować B na podstawie danych z innego miejsca. W api chcesz zwrócić tylko A w zależności od B. I tak na każdym etapie operujesz tylko na tych polach których faktycznie potrzebujesz. Niestety, bawisz się w mapowanie wszystkiego zawsze.

Przy okazji możesz się zainteresować architekturą hexagonalną, ponieważ ładnie do tego pasuje
https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/

U mnie to jest ogarnięte tak

  • warstwa aplikacji - httpy, commandbusy, eventbusy, listenery, utilsy, etc.
  • warstwa domeny - agregaty, vo, serwisy domenowe, inaczej - domain objects
  • warstwa infry - adaptery do portów z warstwy aplikacji i domeny (tam są właściwie same interfejsy) - do perzystencji, zdarzeń itp. Np w tej chwili mając event driven architecture wewnątrz monolitu / jednej aplikacji nie potrzebuję message brokera. Ogarniam to przez ApplicationEventPublisher. Ale o tym wie tylko infra, warstwy wyzej nic o tym nie wiedza, bo wala w interfejs. Jak zaczniemy używać architektury mikroserwisowej, wówczas zmienimy tylko rozszerzenie eventbusa w infrastrukturze, który będzie już walił w rabbita czy inną kafkę.

Co do modeli - w związku z tym, że mam cqrs na full tworzę osobny moduł, który przechwytuje zdarzenia i buduje read model. Wszystkie gety tam mają lecieć. W zależności od kontekstu buduję odpowiedni model widoku. Jeśli w kontekście danego bytu potrzebuję pola A B i C, tak zostawiam. Jeśli mam coś zmienić/przemapować, tworzę osobny widok, itd.

Generalnie w pytaniu chodziło mi o technikalia dotyczące modułów javowych, z którymi nie mam zbyt dużego doświadczenia. Na teraz wydaje mi się, że pakiety są najlepszym kompromisem w tym co chciałbym osiągnąć, nie tracąc jednocześnie ścisłej izolacji modułów wg domeny.

edytowany 1x, ostatnio: hopsey
HO
  • Rejestracja:prawie 5 lat
  • Ostatnio:ponad rok
  • Postów:19
0
Charles_Ray napisał(a):

Odnośnie sposobie podziału na moduły i komunikacji między nimi wydaje mi się to OK. Pamiętaj tylko, że obecnie masz monolit, a wiec jeden JVM. Dodanie większej liczby adnotacji niewiele tutaj pomoże. Pójście w mikroserwisy nie zawsze ma sens i się opłaca - masz potrzebę niezależnego skalowania modułów lub wiele niezależnych zespołów?

niezależnego skalowania modułów, ale jeszcze nie teraz. w tej chwili to byłby zdecydowanie niepotrzebny, dodatkowy narzut. warstwa odpowiadające za kwerendy (mam cqrsa) będzie dosyć mocno orana, w czasie kiedy domena trochę mniej. jestem pewien, że prędzej czy później cały read model trzeba będzie rozstawić na kilku podach.

Charles_Ray
Oki! Brzmi sensownie :) jedno zastrzeżenie - skoro masz CQRS to dlaczego nie możesz zreplikować bazy/indeksu? Po stronie aplikacji nie będzie dużo do roboty
HO
@Charles_Ray: nie wiem czy dobrze rozumiem i czy to właściwa odpowiedź - mam też ES, nie przechowuję modeli w całości (tylko przy domenie - oczywiście tak gdzie crud tam jest crud). Nie da się z nich nic wyczytać. No chyba że jakieś widoki na PG ;)
Charles_Ray
Napisałeś, że read model będzie trzeba skalować, czyli ES nie ma nic do tego ;)
HO
@Charles_Ray: ok, już kumam - tak, może i replikacja db warstwy widokowej by wystarczyła... nie chcę jednak zamykać furtki na smooth przejście w MS :)
nie100sowny
  • Rejestracja:prawie 9 lat
  • Ostatnio:2 dni
  • Lokalizacja:Kraków
  • Postów:402
0
danek napisał(a):

Moduł domenowy: tutaj jakaś logika, filtrowanie cokolwiek, widzi moduł do komunikacji z bazą.
[,,,]
Przy okazji możesz się zainteresować architekturą hexagonalną, ponieważ ładnie do tego pasuje

W architekturze hexagonalnej to moduł bazy danych widzi domenowy, a domenowy nie widzi bazy danych. Zalezność odwrócona.

API --> DOMAIN <-- STORAGE

Twoja domena teraz zależy od szczegółów bazy danych.


"Gdy się nie wie, co się robi, to się dzieją takie rzeczy, że się nie wie, co się dzieje"
edytowany 3x, ostatnio: nie100sowny
danek
widzi w sumie tylko port, ale tak, masz rację 
damianem
  • Rejestracja:prawie 8 lat
  • Ostatnio:4 miesiące
  • Postów:205
2

Według mnie przy tym co chcesz osiągnąć, powinieneś tworzyć osobny application context dla każdego modułu i ustawiać na nim parent context z Twojego main modułu. W taki sposób konteksty modułów powinny mieć dostęp do propertiesów parenta.
Żeby zachować niezależność modułów, dodałbym osobny moduł, nazwijmy go module-api (moduły i main powinny mieć na niego zależność) gdzie umieściłbym taki interface:

Kopiuj
interface MyAppModule {
    ApplicationContext createModuleContext(ApplicationContext parent);
}

Główna klasa w każdym module może wtedy implementować taki interface a główny moduł może poprzez mechanizm SPI ładować dynamicznie zadeklarowane implementacje i inicjalizować moduły, używając createModuleContext i podając swój context jako parent.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.