Produkty Poradenství O nás Blog Kontakt English
arrow_back Zpět na blog

Spring Modulith v praxi: Vynucování architektury v rostoucím monolitu

Spring Modulith v praxi: Vynucování architektury v rostoucím monolitu

Každá rostoucí Spring Boot aplikace dospěje do bodu, kdy někdo nakreslí hranice modulů na tabuli. Krabičky s čistými šipkami. „Venue závisí na Site, ale ne na internals Useru." Všichni přikývnou. O dva sprinty později vývojář importuje repository z modulu, na kterém by neměl záviset, protože je to nejrychlejší cesta k opravě bugu. Nikdo si toho v code review nevšimne. Diagram na tabuli je teď fikce.

Spring Modulith mění ty šipky na tabuli na selhání buildu.

Základní myšlenka

Spring Modulith zachází s každým top-level package pod hlavní application třídou jako s modulem. Každý modul deklaruje, na kterých dalších modulech může záviset. Při spuštění testů Modulith tyto hranice ověří — pokud modul A sahá do internals modulu B bez povolení, build selže.

Java
@ApplicationModule(
    displayName = "Venue Management",
    allowedDependencies = {
        "common",
        "infrastructure :: axon",
        "infrastructure :: security",
        "user :: domain-model",
        "site :: api-queries"
    })
package com.example.venue;

Ta deklarace allowedDependencies je klíčová. Není to dokumentace — je to constraint vynucovaný ArchUnitem pod kapotou. Pokud se modul Venue pokusí importovat cokoliv z modulu Subscription, test selže. Žádné výjimky, žádné „opravím to později."

Named Interfaces: Skutečná hranice

Syntaxe :: v deklaracích závislostí výše odkazuje na named interfaces — podmnožiny veřejného API modulu. Tady se Modulith posouvá z „příjemného doplňku" na skutečně silný nástroj.

Představte si modul Site, který má commandy, eventy, queries a typy doménového modelu. Bez named interfaces vám závislost na modulu Site dá přístup ke všemu — jeho repositories, projekcím, interním službám. To je příliš mnoho.

Named interfaces umožňují vystavit pouze to, co ostatní moduly potřebují:

Java
@NamedInterface("api-commands")
package com.example.site.api.commands;

@NamedInterface("api-queries")
package com.example.site.api.queries;

@NamedInterface("domain-model")
package com.example.site.domain.model;

Když modul Venue závisí na "site :: api-queries", může číst data Site prostřednictvím query objektů. Nemůže přistupovat k Site repository, projection handlerům ani interním službám. Hranice je přesná.

Tohle je filozofie modulithu: komunikujte přes dobře definovaná rozhraní, ne přes sdílené interní detaily.

Vzory, které fungují ve velkém měřítku

Open modul

Každá aplikace má sdílené typy — UserId, PageRequest, společná DTO. Umístěte je do modulu common deklarovaného jako Type.OPEN:

Java
@ApplicationModule(
    displayName = "Common",
    type = ApplicationModule.Type.OPEN)
package com.example.common;

Jakýkoliv modul může záviset na open modulu bez explicitní deklarace. Ale udržujte ho malý. Pokušení nacpat vše sdílené do common je silné — odolávejte mu. Pokud je typ sdílený pouze mezi dvěma moduly, umístěte ho do named interface jednoho z nich.

Infrastruktura jako named interfaces

Cross-cutting concerns — security, DLQ handling, GraphQL konfigurace, reaktivní utility — patří do modulu infrastructure s named interfaces:

Text
infrastructure :: axon
infrastructure :: dlq
infrastructure :: graphql
infrastructure :: security
infrastructure :: reactive

Modul zpracovávající eventy potřebuje infrastructure :: dlq. Modul vystavující GraphQL endpointy potřebuje infrastructure :: graphql. Žádný modul nedostane celou infrastrukturu.

Postupné vynucování

Tohle je vzor, který dělá adopci realistickou. Nepokoušejte se opravit každé porušení hranic před mergem Modulithu. To by oddálilo adopci o měsíce.

Místo toho:

  1. Přidejte @ApplicationModule ke každému package
  2. Spusťte modules.verify() v testu — ale nejdřív bez blokování buildu
  3. Logujte porušení, neblokujte build
  4. Čistěte moduly jeden po druhém
  5. Přesouvejte čisté moduly do vynucované sady
GROOVY
// Moduly s nulovou tolerancí na nová porušení
static final Set<String> ENFORCED_MODULES = [
    'achievement', 'history', 'search', 'tag',
    'route', 'site', 'social', 'venue',
    // ... tento seznam postupně rozšiřujte
]

// Pouze tyto moduly mají stále známá porušení
def allowedSourceModules = ['legacy-auth', 'infrastructure', 'common']

Jakmile je modul v ENFORCED_MODULES, nikdo nemůže zavést nové porušení hranic. Architektura se pouze zlepšuje, nikdy nezhoršuje. To je mechanická výhoda přístupu, kdy se architektura řeší jako testy, ne jako směrnice.

Co se změnilo ve Spring Modulith 2.0

Pokud začínáte na zelené louce, použijte 2.0 (vyžaduje Spring Boot 4). Pokud jste na 1.x, tady je přehled novinek z upgradu.

Přepracovaný Event Publication Registry

Pokud používáte ApplicationEventPublisher pro komunikaci mezi moduly, event publication registry sleduje, které eventy byly zpracovány. Ve verzi 2.0 prošel kompletním přepisem:

  • Granulární stavy publikací — nejen „dokončeno" a „nedokončeno"
  • Monitor zastaralosti — publikace zaseknuté příliš dlouho jsou automaticky označeny jako selhané, což nahrazuje manuální cleanup joby, které si mnoho týmů muselo psát samo
  • Serializovaná externalizace eventů — pokud posíláte eventy do Kafky nebo podobného systému, publikace jsou nyní serializované, aby se předešlo problémům s řazením v multi-instance deploymentech

Flyway migrace specifické pro modul

Každý modul může nyní vlastnit své databázové migrace:

Text
site/src/main/resources/db/site/migration/V1__create_site_tables.sql
venue/src/main/resources/db/venue/migration/V1__create_venue_tables.sql

Konec koordinace číslování migrací napříč celou aplikací. Týmy modulů verzují své schéma nezávisle.

Ověření struktury při startu

V 1.x probíhalo ověření hranic pouze v testech. Ve verzi 2.0 ho můžete zapnout při startu aplikace — pokud modul přistoupí k něčemu, k čemu nemá, aplikace se nespustí. V produkci ho nechte vypnutý (testy stačí a latence startu je důležitá), ale pro lokální vývoj je užitečný.

Ověření hexagonální architektury

Verze 2.0 přidává automatické ověření, že moduly dodržují vzory hexagonální architektury, s výchozím nastavením lenient mode (varuje, ale neselže). To dává smysl — ne každý modul těží z plného ports-and-adapters vrstvení a vynucování na jednoduchých CRUD modulech by bylo over-engineering.

Modulith jako pojistka při AI-asistovaném vývoji

Tohle je výhoda, o které zatím nikdo nemluví.

AI coding agenti jsou rychlí a produktivní, ale mají zásadní slabinu: optimalizují na to, aby kód fungoval, ne na to, aby dodržoval architektonické hranice. AI agent požádaný o „přidání notifikace při přihlášení uživatele" klidně importuje interní repository modulu subscription z modulu notification, pokud je to nejkratší cesta k fungujícímu řešení.

Bez Modulithu si tohoto cross-module úniku nikdo nevšimne do code review — pokud ho vůbec zachytíte — nebo dokud se graf závislostí nestane nerozpletitelným chaosem. S Modulithem build selže. Agent dostane selhání testu a je nucen najít architektonicky správnou cestu: dispatchovat query, záviset na named interface, nebo naslouchat eventu.

Co jsme v praxi pozorovali:

Agenti respektují hranice, které vidí v chybových zprávách. Když AI agent zkusí importovat com.example.subscription.internal.SubscriptionRepository z jiného modulu a dostane Modulith violation, sám se opraví. Chybová zpráva mu doslova říká, co je povoleno.

package-info.java je prompt pro agenty. Když AI agent vytváří nový modul, kopíruje vzor @ApplicationModule z existujících modulů. Deklarace allowedDependencies ho nutí přemýšlet o tom, co modul skutečně potřebuje.

Test modularity zachytí to, co code review přehlédne. Lidští revieweři se soustředí na logiku, ne na importy. AI agent může vygenerovat 500 řádků správné business logiky s jedním nelegálním cross-module importem na řádku 12. Test modularity ho zachytí pokaždé.

Postupné vynucování vytváří jednosměrný mechanismus. Jakmile je modul vynucovaný, žádný agent ani člověk nemůže zavést nové porušení hranic. Architektura se časem pouze zlepšuje.

Ironie: AI agenti dělají Modulith cennějším, ne méně cenným. Čím rychleji generujete kód, tím důležitější jsou automatizované guardraily. Manuální code review neroste s rychlostí AI-asistovaného vývoje. Automatizované ověření hranic ano.

Co Modulith nedělá

Modulith vynucuje strukturální hranice — které třídy mohou odkazovat na které. Nevynucuje:

  • Runtime izolaci — moduly sdílejí JVM, classloader a transakční kontext
  • Datovou izolaci — moduly mohou sdílet databázi; vlastnictví schématu je konvence, ne vynucované pravidlo
  • API verzování — když změníte sdílený typ, všechny moduly se rekompilují; neexistuje nezávislý deployment
  • Izolaci chyb — stack overflow v jednom modulu položí celou aplikaci

Pokud tyto vlastnosti potřebujete, potřebujete mikroslužby. Modulith je pro týmy, které chtějí architektonickou disciplínu bez operační složitosti.

Měli byste ho adoptovat?

Ano, pokud:

  • Máte 5+ logických modulů ve Spring Boot aplikaci
  • Více vývojářů nebo týmů pracuje ve stejném codebase
  • Chcete vynucovat pravidla závislostí bez operační režie mikroslužeb
  • Používáte AI coding asistenty a chcete guardraily proti erozi architektury

Zatím ne, pokud:

  • Vaše aplikace je opravdu malá (méně než 5 modulů, 1–2 vývojáři)
  • Nejste ještě na Spring Boot 4 (použijte Modulith 1.4.x na Boot 3)
  • Vaše hranice modulů jsou dobře udržované konvencí — a jste si jistí, že tak zůstanou

Investice je malá: několik souborů package-info.java, jeden verifikační test a rozhodnutí, které moduly vynucovat jako první. Výsledek? Váš architektonický diagram přestane být fantazií na tabuli a stane se build constraintem.


Titulní foto od Martin Woortman na Unsplash.

Další z blogu