Application architecture¶
Targets: net10.0, csharp-14 · Last reviewed: 2026-09-14 · Sources: milan-jovanovic, code-with-mukesh, mark-seemann, ms-learn, derek-comartin
Start with a modular monolith. Organise the code inside each module as vertical slices, and use layered (Clean) architecture only inside the slices that have real domain rules to protect.
Opinions¶
- Default to a modular monolith, not microservices. Modules draw the macro boundaries: where the business boundaries are, who owns which data, how modules talk, and what public API each one exposes. That is the decision worth making early; a process boundary is a deployment choice you can make later, once a module has earned it. (Jovanović: Modular monolith architecture in .NET)
- Organise the inside of a module as vertical slices. One use case owns its endpoint, request, validation, business logic, and data access together, so a feature changes in one place instead of across a controller, a service, and a repository that exist only to separate technologies. (Jovanović: Vertical slice architecture in .NET)
- Vertical slices are not modules. Slices organise behaviour within a boundary; modules are the boundary. Treating a slice as a module gives you a folder structure with none of the ownership guarantees. (Jovanović: Where vertical slices fit inside the modular monolith)
- Apply Clean Architecture per slice, not per solution. Layering controls dependency direction, which is worth paying for where domain rules must stay independent of infrastructure, and pure overhead in a slice that reads a row and returns it. Force every slice through identical layers and you get four projects to change a
GET. Seemann priced that overhead on a read-only feature: one new field cost six edits, spread across the schema, the persistence type, the domain type, the view model, and the two mappings between them. His conclusion was that a CRUD-shaped application should not be layered at all. (Jovanović: Vertical slice architecture in .NET, Mukesh: Clean Architecture in .NET 10, Seemann: Is Layering Worth the Mapping?) - Make a module boundary something the compiler propagates, because a documented one gets crossed. A boundary that reads like an ordinary method call is treated as one, which is how WCF invited round trips through code that looked local. Asynchrony is the mechanism already to hand:
asyncis contagious, so every caller up the stack has to acknowledge the crossing, and no helper method can wrap it back out of sight. (Seemann: Boundaries are Explicit) - Publish integration events between modules; keep domain events inside the module that raised them. A domain event is an implementation detail, so exposing it couples consumers to your internal model as surely as publishing the schema would. The two differ in implementation rather than meaning: a domain event dispatches in-process within one model, an integration event is always asynchronous and crosses the boundary. Translate upward as you cross it, publishing one
OrderDispatchedrather than theTruckReservedandCarrierAssignedsteps that produced it, and name the event for the business fact:TruckOrderNotUsedand a generic cancellation settle differently downstream. (Microsoft Learn: Domain events: design and implementation, 2018 content last reviewed 2024-01-03; Comartin: Domain Events Are NOT Your Public API, which carries an NServiceBus sponsorship in its body) - Let the folder structure name the feature, not the pattern.
Orders/Cancel/beatsControllers/,Services/,Repositories/. The repository layout opinions in project-structure.md stop at the project boundary; inside a project, features are the top-level grouping.
Source redundancy¶
The file opened on two sources admitted the same day under the lowered longevity bars (milan-jovanovic with a conflict-of-interest note, code-with-mukesh marked Corroborate.), both of which sell templates and courses built on exactly these patterns. mark-seemann and ms-learn now stand beside them, unmarked and with nothing to sell on this topic, so the opinion's central claims no longer rest on commercially interested sources alone. derek-comartin is marked Corroborate. and takes NServiceBus sponsorship inside the article body, which is why the event bullet leads on Microsoft Learn and flags the sponsorship where a reader will see it.
Treat anything stronger than the above as still unsourced: prescribed folder names, mediator libraries, per-slice project counts.