Modulith (Modular Monolith)
A single deployment, but internal modules with strict boundaries and explicit communication.
Context
You want the simplicity of a monolith and the modular autonomy of microservices — without the distributed operational overhead.
Solution
- Each module (Sales, Billing, Shipping) is a bounded context.
- Communication via public interfaces or in-process events.
- Each module owns its own tables; others do not read them.
- Dependency rules enforced with static analysis (NetArchTest, ArchUnitNET).
#C# Example
CSHARP
src/
├── Modules/
│ ├── Sales/
│ │ ├── Sales.Public/ ← external contracts
│ │ ├── Sales.Application/
│ │ ├── Sales.Domain/
│ │ └── Sales.Infrastructure/
│ ├── Billing/
│ │ └── …
│ └── Shipping/
│ └── …
└── Bootstrap/ ← composes DI, exposes HTTP
// Inter-module communication: in-process events
public record OrderPlacedIntegrationEvent(Guid OrderId, decimal Total);
// Sales publishes:
await _events.PublishAsync(new OrderPlacedIntegrationEvent(order.Id, order.Total));
// Billing listens (same process, no network):
public class CreateInvoiceOnOrderPlaced : INotificationHandler<OrderPlacedIntegrationEvent> { /* ... */ }#Why it matters
If tomorrow you need to extract Billing into a microservice, you just change the in-process bus for a real one (RabbitMQ/Kafka). The refactor exists within the module.
When NOT to use it
- When you already have completely independent teams with different stacks.
- When one module needs to scale 100x more than the rest.
Tradeoffs
| Pro | Con |
|---|---|
| Operational simplicity of a monolith | Team discipline required |
| Clear path to microservices | Tooling for boundary enforcement |
| Real transactions between modules (if allowed) | Temptation to bypass boundaries |
#architecture #modulith