Modularny monolit - jak najlepiej ugryźć?

0

Cześć,

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

|
|--- 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

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

@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!

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
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?

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.

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/

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.

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?

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.

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.

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.

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:

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.

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