Why Modular Monolith?
A modular monolith combines the simplicity of monolithic deployment with the organizational benefits of microservices. It's an excellent starting point for most projects.
Key Principles
Strong Module Boundaries
Each module owns its data and exposes only a public API to other modules:
Modules/
โโโ Orders/
โ โโโ Public/ # Public API
โ โ โโโ IOrderService.cs
โ โ โโโ OrderDto.cs
โ โโโ Internal/ # Hidden implementation
โ โโโ OrderService.cs
โ โโโ Order.cs
โ โโโ OrderRepository.cs
โโโ Inventory/
โโโ Customers/Module Communication
Modules communicate through well-defined interfaces, never accessing each other's internals:
// Orders module needs customer data
public class OrderService {
private readonly ICustomerService _customers; // From Customers module
public async Task CreateOrder(CreateOrderRequest request) {
var customer = await _customers.GetById(request.CustomerId);
// Create order with customer info
}
}Separate Databases (Logical)
While sharing a physical database, each module owns its tables. Use schemas or naming conventions:
-- Orders schema
CREATE TABLE orders.orders (...)
CREATE TABLE orders.order_items (...)
-- Customers schema
CREATE TABLE customers.customers (...)
CREATE TABLE customers.addresses (...)Module Structure
public class OrdersModule : IModule {
public void RegisterServices(IServiceCollection services) {
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IOrderRepository, OrderRepository>();
}
public void ConfigureEndpoints(IEndpointRouteBuilder routes) {
routes.MapGet("/api/orders", GetOrders);
routes.MapPost("/api/orders", CreateOrder);
}
}Event-Based Integration
Use in-process events for loose coupling between modules:
// Orders module publishes
await _mediator.Publish(new OrderPlaced(order.Id));
// Inventory module subscribes
public class OrderPlacedHandler : INotificationHandler<OrderPlaced> {
public async Task Handle(OrderPlaced notification) {
await _inventory.ReserveStock(notification.Items);
}
}Benefits
- Single deployment unit - simple operations
- In-process communication - fast and reliable
- Shared infrastructure - lower costs
- Clear boundaries - organized codebase
- Future-ready - can extract to microservices later
Migration Path to Microservices
When a module needs independent scaling:
- Replace in-process events with message broker
- Extract module to separate service
- Add API gateway for routing
- Separate database if needed
Conclusion
Start with a modular monolith. You get development velocity now with a clear path to microservices if and when you need them.