The Problem
A natural first step when building a modular monolith is to put each module in its own class library project. Modules then reference each other’s projects directly whenever they need something.
This works at first, but real business workflows don’t flow in one direction. Customers have Shopping Carts, Carts create Orders, Orders use Shipping, and Shipping needs to contact the Customer. With a single project per module, nothing prevents these references from looping back on themselves.
Circular dependencies tightly couple every module in the cycle. You can’t change, test, or extract one of them without dragging the others along. In .NET, project references can’t be circular at all, so teams often resort to merging modules or adding workarounds that blur the boundaries they were trying to create.
The Solution
Split each module into two projects:
- The module project holds the implementation: domain model, data access, and business logic. It is internal to the module.
- The contracts project holds only the types other modules are allowed to use: commands, queries, DTOs, and integration events.
Each module references its own contracts project. When a module needs something from another module, it references that module’s contracts project, never its implementation.
Notice the direction of the arrows. Module projects only have arrows pointing out, toward the contracts they depend on. Contracts projects only have arrows pointing in. Because contracts never reference module implementations, a cycle between modules can’t form. This is the Dependency Inversion Principle applied at the module level.
Guidelines
Keep Contracts Small
Expose only what other modules actually need. Every public type in a contracts project is a promise you have to keep.
Keep Contracts Stable
Other modules depend on your contracts, so they should change less often than your implementation. This follows the Stable Dependencies Principle.
Keep Contracts Free of Dependencies
Contracts projects should not reference module implementations, and ideally nothing beyond a small shared kernel.
Benefits
- No circular dependencies - Modules depend on contracts, and contracts depend on nothing
- Explicit public API - It’s obvious which types a module exposes to the rest of the system
- Enforced by the compiler - Module internals simply aren’t referenced, so they can’t be used by accident
- Path to microservices - A contracts project maps naturally to the API or message types a service would expose