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