The Myth of Microservices for Early-Stage Products
A defense of the well-structured modular monolith and a pragmatic rubric for when it actually makes sense to fracture service boundaries.
Few architectural decisions have burned more early-stage engineering capital than prematurely splitting a codebase into dozens of microservices.
Engineers look at Netflix, Uber, or Amazon and assume that splitting code into services is what made them scale. In reality, microservices were designed to solve organizational scaling problems (thousands of engineers stepping on each other's toes), not computational performance problems.
The Hidden Tax of Microservices
When an early-stage startup adopts microservices with an engineering team under 20 people, they pay a crushing tax:
- Distributed Tracing & Telemetry: Instead of reading a single stack trace, you need OpenTelemetry collectors, Jaeger spans, and correlated log IDs across 8 repositories.
- Data Consistency: Forget ACID transactions. Every cross-boundary write now requires sagas, compensating transactions, outbox patterns, and eventual consistency handling.
- Deployment Friction: Local development requires running 10 Docker containers, eating 16GB of RAM before writing a single line of code.
The Pragmatic Alternative: The Modular Monolith
A modular monolith enforces strict domain boundaries inside a single deployment artifact:
src/
├── billing/ # Owns invoices, cards, charges
│ ├── internal/ # Private domain logic
│ └── api.go # Public exported interface
├── catalog/ # Owns products, pricing, stock
└── identity/ # Owns auth, permissions, users
Modules communicate exclusively through typed in-memory interfaces, not network sockets. You retain zero-latency function calls, atomic database transactions, and simple CI/CD pipelines.
When a specific module genuinely requires distinct hardware (e.g. GPU compute for video rendering), extracting it is trivial because its interface is already decoupled.