Modular Monolith: The Best of Both Worlds
A Modular Monolith combines the deployment simplicity of a monolith with the organizational benefits of microservices.
TL;DR
| Concept | Definition |
|---|---|
| Core Idea | Single deployable with strong module boundaries |
| Modules | Independent bounded contexts within one application |
| Communication | In-process, but through defined interfaces |
| Evolution | Can extract modules to services later |
Structure
src/
├── Modules/
│ ├── Orders/ # Orders bounded context
│ │ ├── Api/
│ │ ├── Domain/
│ │ └── Infrastructure/
│ ├── Inventory/ # Inventory bounded context
│ └── Shipping/ # Shipping bounded context
├── Shared/
│ ├── Shared.Kernel/ # Shared domain concepts
│ └── Shared.Contracts/ # Integration events
└── Host/
└── Program.cs # Wires everything together
Module Rules
Rule 1: Modules Have Clear Boundaries
<!-- Orders.csproj - NO reference to Inventory! -->
<ProjectReference Include="Shared.Kernel.csproj" />
<ProjectReference Include="Shared.Contracts.csproj" />
Rule 2: Communication Through Contracts
// Orders publishes
await _eventBus.Publish(new OrderPlacedIntegrationEvent(order.Id, ...));
// Inventory subscribes
public class OrderPlacedHandler : IEventHandler<OrderPlacedIntegrationEvent>
{
public async Task Handle(OrderPlacedIntegrationEvent @event)
{
await _inventoryService.Reserve(@event.ProductId, @event.Quantity);
}
}
Rule 3: No Direct Database Access
// ❌ Wrong
return await _dbContext.Orders.FindAsync(orderId); // Accessing Orders from Inventory!
// ✅ Right
return await _ordersModule.GetOrder(orderId); // Use public interface
Rule 4: Modules Own Their Data
modelBuilder.HasDefaultSchema("orders"); // Orders schema
modelBuilder.HasDefaultSchema("inventory"); // Inventory schema
Extraction to Microservices
The modular monolith makes extraction easy:
- Module has clear public interface
- All communication is through events/APIs
- No shared database tables
- Replace in-process event bus with message queue
Trade-offs
| Benefit | Trade-off |
|---|---|
| Simple deployment | Single point of failure |
| Fast communication | Modules can't scale independently |
| Easier debugging | Large codebase |
| Transaction simplicity | Must maintain boundaries |