Modular Monoliths in .NET – Scalable Without Microservices
Microservices aren’t the only way to achieve scale. A well-designed modular monolith lets you ship features quickly, keep runtime simple, and still split into services later—if you ever need to.
Real-Life Analogy: The Department Store
Imagine a large department store. Inside one building you have Clothing, Electronics, and Homeware—each with its own staff, stockroom, and tills. Customers feel it’s one seamless shop, but departments operate semi-independently. If Electronics grows too big, you can spin it off into its own storefront without rebuilding the others.

Why Not Jump Straight to Microservices?
- Operational Overhead —multiple deployables, Dockerfiles, pipelines, monitoring stacks.
- Distributed Failures —network calls, retries, eventual consistency.
- Cost —extra infrastructure for each tiny service.
Unless you have distinct load profiles or organisational reasons, a single deployable is faster and cheaper—if it stays modular.
Defining Modules
- Autonomous Domain —owns its data and business rules.
- Internal Cohesion —high coupling within the module.
- External Contracts —other modules can interact only through clear interfaces or messages.
Folder & Project Layout
src/
├─ Modules/
│ ├─ Ordering/
│ │ ├─ Ordering.Application/
│ │ ├─ Ordering.Domain/
│ │ └─ Ordering.Infrastructure/
│ └─ Inventory/
│ ├─ Inventory.Application/
│ ├─ Inventory.Domain/
│ └─ Inventory.Infrastructure/
└─ WebApi/ # ASP.NET Core host with only references to *.Application projects
Inter-Module Communication
Inside one process, MediatR
or a simple in-memory message bus keeps modules ignorant of each other’s
internals:
// Cross-module notification
public record ProductReserved(Guid ProductId, int Quantity) : INotification;
public class InventoryHandler : INotificationHandler<ProductReserved>
{
public Task Handle(ProductReserved evt, CancellationToken ct)
{
// adjust stock levels...
return Task.CompletedTask;
}
}
Database Strategy
- Schema-per-Module —same physical DB, separate schemas; easiest to start.
- DB-per-Module —logical isolation, still one connection string.
- Event-carry State —emit integration events for true eventual consistency.
Evolution Path to Microservices
- Identify a module with unique scaling or deployment needs.
- Extract its data to its own physical database.
- Wrap the module behind an HTTP/gRPC façade; other modules switch to network calls.
- Move the code into a separate repo and deploy independently.
Common Pitfalls
- Leaky Dependencies: a module reaches into another’s data models “just this once.” Don’t.
- Shared Infrastructure Code: keep cross-cutting concerns (logging, auth) in a shared package, not as random static classes referenced everywhere.
- Premature Splitting: extract only when metrics or team structure demand it.
Final Thoughts
A modular monolith offers 80 % of the independence of microservices at a fraction of the complexity. Keep your boundaries strict, your contracts lean, and your modules cohesive; scalability will follow—without the pager-nightmares of distributed debugging.