A modular monolith without enforcement is not an architecture — it is a monolith with good intentions.
The Problem Most teams skip the modular monolith and jump straight to microservices.
The ones that do attempt a modular monolith rely on convention — "don't cross module boundaries" — which fails the moment deadlines hit.
The difference between a well-structured modular monolith and a mess is whether boundaries are maintained by tooling or by convention.
The Solution Structure Each module is a pair of .NET projects: The rule: modules may only reference each other's projects.
The compiler enforces this physically — no project reference means no type access.
Four Layers of Enforcement Compiler — project references prevent cross-module type access NetArchTest — architecture tests fail the build on namespace-level violations NDepend CQLinq — catches dependency cycles and coupling the compiler can't see Quality Gates — block PRs that introduce new boundary violations Module-Scoped Data Each module owns a dedicated with a schema prefix (, ).
No module queries another module's tables.
Cross-Module Communication Modules communicate via MediatR in-process events.
Orders publishes ; Payments subscribes — without Orders knowing Payments exists.
This is also the extraction seam: when you eventually extract a module into a service, MediatR becomes a message broker.
The event contract stays the same.
Parallel CI Each module's tests run in parallel.
CI time scales with the slowest module, not the total count.
The Extraction Path When a module genuinely needs independence: Add outbox table → publish to real broker Replace MediatR handlers with broker consumers Deploy module as separate service Publish as NuGet package The boundary was already clean.
Extraction is a deployment change, not a redesign.
The full post covers NDepend CQLinq rule examples, Quality Gate configuration, GitHub Actions pipeline YAML, test isolation patterns, and a production checklist. 👉 Read the complete implementation guide