Series: Architecture in Practice
A field report from six months of building a modular .NET backend with one team, based on presentations I’ve given at work. It isn’t a tutorial. The concepts are explained better elsewhere, and the posts link to where. This series is about what the architecture looks like in a real codebase: what we started with, what changed, and which rules the build enforces now. It’s for developers who already know the basics of domain-driven design and ports and adapters.
- Four Models of One Person: domain-driven design, from the glossary to the rules inside a module.
- Ports and Adapters Without Exceptions: the layout, the naming and the architecture tests that enforce it.
- Bridges Ask, Events Tell: how modules talk to each other without knowing each other.
- Types as Proof: parsing at the boundary, so rules can’t be skipped.
Cheat sheet
Where does it go?
| What | Where |
|---|---|
| A business rule, aggregate, value object or domain event | Core |
| Something the application can do, one per use case | Application/Ports/Driving/<Action>/: the port, its command and its response |
| The code that does it | Application/UseCases/<Action>UseCase, internal and sealed |
| Something a use case needs from outside | Application/Ports/Driven/IFor…ing |
| Logic shared by use cases and implemented in Application | Application/Services. It isn’t a port |
| HTTP endpoints, background jobs, integration event handlers | Infrastructure, as driving adapters |
| Database, scheduler, configuration | Infrastructure, as driven adapters |
| Asking another module for facts | A bridge in your Infrastructure, calling that module’s Contracts |
| What other modules may call or hear about | Your Contracts, implemented by contract adapters |
Naming
| Thing | Example |
|---|---|
| Driving port | ISubmitTicketPort, with SubmitTicketCommand and SubmitTicketResponse |
| Use case | SubmitTicketUseCase |
| Driven port | IForStoringTickets: what the application needs, not the technology |
| Contract | IContractForFetchingTopic |
| Integration event handler | OnTicketSubmittedCreateAssignment |
Adding a feature
- The behaviour and its domain event in Core, with tests.
- A use case test with test doubles for the driven ports. It’s the first driver, so it shapes the command, the response and the names.
- The driving port, with a command made of primitives.
- The use case: parse the input, call the domain, store, commit.
- Reuse existing driven ports. Add a new
IFor…ingport and its adapter only when nothing fits. - Map the domain event to an integration event, if other modules should know.
- The driving adapter last: an endpoint, a job or an event handler.
Rules the build enforces
- Core doesn’t depend on Application or Infrastructure.
- Application doesn’t depend on Infrastructure.
- No database or scheduler packages in Core or Application.
- Modules only meet through Contracts.
- Driven ports never take or return a Guid.
- Value objects keep their state private.
- Use cases and adapters are internal and sealed.
The test for the Guid rule is in part 2.
Further reading
- Alistair Cockburn, Hexagonal Architecture: the original description of ports and adapters.
- Herberto Graça, Explicit Architecture: how DDD, hexagonal, onion and clean architecture fit together.
- Martin Fowler, Domain-Driven Design: a short overview that links to the key concepts.
- Eric Evans, Domain-Driven Design: the blue book, where DDD started.
- Vaughn Vernon, Implementing Domain-Driven Design: the red book, on putting DDD into practice.
- Kamil Grzybek, Modular Monolith with DDD: a complete .NET sample application.
- Oskar Dudycz, Outbox, Inbox patterns and delivery guarantees explained.
- Alexis King, Parse, don’t validate.
- Scott Wlaschin, Domain Modeling Made Functional: designing with types, in F#.
- Jimmy Bogard, Vertical Slice Architecture: the main alternative to the approach in this series.