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.
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/ ← everythingTo 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 typesEach 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
reportingmodule may not touchfees' 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.
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.
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.