# De modulaire monoliet en hoe je de grenzen bewaakt

URL: https://brainpowergroup.eu/nl/blog/de-modulaire-monoliet
Datum: 2025-10-28 (bijgewerkt 2026-01-21)
Door: Aleksa, Brainpower Group

Een modulaire monoliet is één deployment met modules per domein, elk met een publieke interface en eigen tabellen. Controles in de CI houden de grenzen overeind.

Een modulaire monoliet is één applicatie met één deployment, intern opgedeeld in modules per domein. Elke module heeft een publieke interface en eigen tabellen, en andere modules gebruiken alleen die interface. De grenzen bewaak je met tooling in de CI, want zonder automatische controle slijten ze weg.

## Waarom een modulaire monoliet

Je houdt de eenvoud van één deployment. Er is één pipeline, één databaseserver en één plek om logs te lezen, en een transactie over twee modules is gewoon een databasetransactie. Tegelijk krijg je de grenzen die je bij microservices zoekt: een team of developer kan aan een module werken zonder de rest te hoeven kennen.

Shopify beschreef deze aanpak in 2019. Het bedrijf hield alle code in één codebase en dwong grenzen tussen componenten af, zonder er losse services van te maken. Erik legt de bredere afweging uit in [monoliet of microservices](/nl/blog/monoliet-of-microservices).

## Modules per domein in plaats van per laag

Veel codebases zijn opgedeeld per technische laag: een map met controllers, een map met services en een map met repositories. Een wijziging aan facturatie raakt dan drie mappen, en niets houdt je tegen om vanuit orders rechtstreeks een facturatie-repository aan te roepen.

Deel daarom op per domein, met een map per module: orders, facturatie, catalogus, klanten. Binnen een module mag je weer lagen hebben. Shopify deed hetzelfde en verdeelde de code in domeinen zoals orders, verzending, voorraad en facturatie.

Begin met weinig modules. Vier tot zes grote domeinen is voor de meeste producten genoeg. Opsplitsen kan later nog, samenvoegen is meer werk.

## Elke module heeft een publieke interface

Andere modules praten met een module via een klein aantal publieke klassen of functies. De rest is intern. Gebruik daarvoor wat je taal al heeft: `internal` in C#, package-private klassen in Java, en in TypeScript exporteer je alleen via het `index.ts`-bestand van de module.

Deel geen entiteiten tussen modules. Orders krijgt geen `Invoice`-object van facturatie, maar een id of een eenvoudig DTO. Zo kan facturatie zijn eigen model aanpassen zonder dat orders breekt.

Modules praten op twee manieren met elkaar:

- Een directe aanroep via de publieke interface, als je een antwoord nodig hebt.
- Een event binnen het proces, voor bijwerkingen. Orders publiceert dat een order geplaatst is, en facturatie maakt daarop een factuur.

Gebruik events niet voor alles. Als elke stap een event is, kan niemand meer volgen wat er na een klik gebeurt.

## Eigen tabellen per module

Eigen tabellen maken volgens mij het grootste verschil, en ze zijn ook het makkelijkst om over te slaan. Geef elke module een eigen schema in de database, bijvoorbeeld `orders` en `billing` in Postgres. Een module leest en schrijft alleen in zijn eigen schema.

De regels die ik aanhoud:

- Geen joins tussen schema's in de applicatiecode. Heb je data van een andere module nodig, vraag die dan op via de interface.
- Geen foreign keys tussen modules. Sla het id op en laat de andere module de waarheid bewaken.
- Migraties per module, in de map van die module.

Rapportages zijn de uitzondering die je bewust regelt. Bouw daarvoor een apart leesmodel of views die van een eigen rapportagemodule zijn, zodat niet elke module overal in leest.

## Grenzen afdwingen in de CI

Afspraken die alleen in een wiki staan, houden het niet lang vol. Zet de regels in code en laat ze bij elke pull request draaien.

| Stack | Tool | Wat het doet |
|---|---|---|
| Java | ArchUnit | Architectuurregels als unittests, zoals afhankelijkheden tussen packages en cycli |
| Spring Boot | Spring Modulith | Controleert de modulestructuur en maakt documentatie van de modules |
| .NET | ArchUnitNET | Architectuurregels in C# als geautomatiseerde tests |
| JavaScript en TypeScript | dependency-cruiser | Controleert imports tegen je eigen regels en meldt overtredingen |
| Nx-monorepo | @nx/enforce-module-boundaries | Lintregel die imports beperkt op basis van tags per project |

Begin met twee regels: geen cycli tussen modules, en geen imports van andermans interne code. Dat vangt het meeste af.

Zet daarnaast per modulemap een regel in je CODEOWNERS-bestand, zodat de eigenaar van een module elke wijziging erin ziet. Hoe je reviews tussen locaties goed regelt, beschrijft Erik in [code review op afstand](/nl/blog/code-review-op-afstand).

## Waar de grenzen toch vervagen

Een gedeelde map met de naam common, shared of utils groeit uit tot een tweede monoliet. Zet daar alleen technische hulpcode in, nooit domeinlogica.

Een module die "core" heet en waar alles van afhangt, is in mijn ogen meestal een domein dat nog niet is opgesplitst. Kijk wat erin zit en verdeel het over de echte modules.

Te vroeg te veel modules maken is ook een valkuil. Twintig modules van een paar bestanden kosten meer coördinatie dan ze opleveren.

## Een module eruit halen

Soms moet een module toch een eigen service worden, bijvoorbeeld omdat hij los moet schalen, een eigen team krijgt of in een ander ritme moet releasen. Met een eigen interface en eigen tabellen is dat goed te doen.

Je zet de module in een eigen service, vervangt de implementatie achter de interface door een HTTP-client of een queue, en verhuist het schema naar een eigen database. Events binnen het proces worden berichten op een queue. De andere modules merken daar weinig van, want die praatten al via de interface. Wel kan een aanroep nu falen of traag zijn, en is een transactie over twee modules geen gewone databasetransactie meer. Of die nieuwe service daarna in dezelfde repository blijft, is een aparte keuze. Erik schreef daarover in [monorepo of losse repositories](/nl/blog/monorepo-of-losse-repositories).

## Bronnen

- [Shopify Engineering: Deconstructing the Monolith (2019)](https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity)
- [ArchUnit](https://www.archunit.org/)
- [ArchUnitNET op GitHub](https://github.com/TNG/ArchUnitNET)
- [Spring Modulith](https://spring.io/projects/spring-modulith)
- [dependency-cruiser op GitHub](https://github.com/sverweij/dependency-cruiser)
- [Nx: enforce module boundaries](https://nx.dev/docs/features/enforce-module-boundaries)

## Veelgestelde vragen

**Wat is een modulaire monoliet?**

Een applicatie die als één geheel wordt uitgerold, maar intern is opgedeeld in modules per domein. Elke module heeft een publieke interface en eigen tabellen, en andere modules mogen alleen via die interface met de module praten.

**Hoe dwing je modulegrenzen af?**

Met architectuurtests of lintregels die in de CI draaien, zoals ArchUnit voor Java, ArchUnitNET voor .NET en dependency-cruiser of de Nx-regel enforce-module-boundaries voor TypeScript. Een pull request die een grens schendt, faalt dan.

**Kun je een module later als losse service uitrollen?**

Ja, als de module een eigen interface en eigen tabellen heeft. Je vervangt dan de aanroepen binnen het proces door netwerkaanroepen of berichten en verhuist de tabellen naar een eigen database.

---

Bron: https://brainpowergroup.eu/nl/blog/de-modulaire-monoliet
Brainpower Group, contact: https://brainpowergroup.eu/nl/contact, hello@brainpowergroup.eu
