Loslegen
Javaneer
Zurück zur Stufe
Stufe 4·Der modulare Monolith

Module, keine Schichten

Nach Feature-/Domänenmodul (vertikale Slices) organisieren statt nach technischen Schichten (controllers/, services/, repos/) - damit eine Änderung an einem Ort lebt und Module später zu Diensten werden können.

14 Min. LesezeitFortgeschritten

Deutsche Übersetzung in Arbeit

Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.

Auf dieser Seite

If a modular monolith's power is its modules, the first question is: what is a module? The instinct from years of tutorials is to organize by technical layer - a controllers/ package, a services/ package, a repositories/ package. That's the wrong axis. A modular monolith organizes by feature or domain - a billing/ module, a catalog/ module, each containing its own controllers, services, and repositories. This single reorientation is what makes boundaries real.

Layers vs. modules: the two axes

Consider ledger-legacy's package structure, the classic layered layout:

com.ledger
├── controllers/   ← LedgerController, AccountController, ReportController, FeeController...
├── services/      ← LedgerService, AccountService, ReportService, FeeService...
├── repositories/  ← LedgerRepo, AccountRepo, ReportRepo, FeeRepo...
└── models/        ← everything

To add a "disputes" feature you touch every folder, and everything in services/ can call everything else - no boundaries at all. Now the same code organized by feature module:

com.ledger
├── accounts/      ← AccountController, AccountService, AccountRepository, Account
├── transactions/  ← TransactionController, TransactionService, ..., Transaction
├── fees/          ← FeeController, FeeService, FeePolicy...
├── reporting/     ← ReportController, ReportService...
└── shared/        ← truly cross-cutting types

Each module is a vertical slice owning its whole stack for one capability. Adding "disputes" means adding one disputes/ module - a change lives in one place. And critically, a module is now a candidate to become a microservice later; a layer never is.

Why vertical beats horizontal

Organizing by feature has concrete advantages the layered layout can't offer:

  • Change locality (high cohesion). A feature's code lives together, so a change touches one module instead of fanning across four technical folders. You navigate by what the code does, not what kind of code it is.
  • Boundaries you can enforce. "The reporting module may not touch fees' internals" is a meaningful, checkable rule. "Controllers may not touch repositories" is a weak layering guideline that says nothing about which feature is coupling to which.
  • Screaming architecture. The top-level packages announce what the system does (accounts, transactions, fees) - not what framework it uses (controllers, services). A newcomer sees the business capabilities at a glance.
  • A migration path. Because a module is a self-contained capability, extracting it into a service (later lesson) is tractable. You can't extract "the services layer" into a service.

Layers still exist - inside a module

This isn't 'layers are bad.' Within the billing module you'll still have a controller, a service, and a repository - horizontal layers are a fine way to organize the insides of a module. The point is which axis is the TOP-LEVEL organizing principle. Feature-first at the top, layered within: billing/ contains its own thin controller/service/repository, and so does catalog/. You get both, in the right order.

Drawing module boundaries

Where do modules come from? From the domain - specifically, the bounded contexts of the DDD module. A bounded context is a natural module boundary: billing, catalog, shipping are contexts and modules. Look for the same signals: where the language shifts, where different rules and change-rates cluster, where a subset of the data is cohesive. Good module boundaries are just bounded contexts realized in package structure - which is exactly why they later make clean service boundaries too.

A department store: by department, not by floor-material

Imagine a department store organized by material - one floor for everything wooden, one for everything metal, one for everything fabric. To buy a complete outfit you'd trek across three floors, and 'the fabric floor' means nothing to a shopper. That's layering. Real stores organize by department - menswear, electronics, homeware - each a vertical slice with its own displays, staff, and stockroom. You find everything for one need in one place (change locality), each department has a clear identity and boundary, and if electronics outgrows the store you can spin it off into its own shop (extract a service). Organizing software by feature module is choosing departments over floor-materials.

Re-slice the package structure

ledger-legacy is organized as controllers/, services/, repositories/, models/. Product wants a new 'fraud detection' capability that scores transactions and flags accounts. Describe how you'd add it in a layered structure versus a feature-module structure, and explain which makes the boundary enforceable and the future extraction possible.

What's the top-level organizing principle of a modular monolith?

Key takeaways

  • Organize the monolith top-level by feature/domain module (vertical slices), not by technical layer - each module owns its own controller, service, and repository.
  • Feature-first gives change locality (a change lives in one module), enforceable boundaries, and 'screaming architecture' that announces what the system does.
  • Layers aren't wrong - they organize the inside of a module; the point is which axis is the top-level organizing principle.
  • A feature module can later be extracted into a microservice; a technical layer never can.
  • Module boundaries come from the domain's bounded contexts - the same seams that make good service boundaries later.
War diese Lektion hilfreich?
Diese Seite auf GitHub bearbeiten