From monolith to modular monolith — when to skip microservices
Microservices were the default architectural answer for a decade. In 2026 most teams that adopted them are walking back. The modular monolith is a saner middle ground for teams of 5-50 engineers.
From 2014 to 2022 every architecture talk pushed microservices. By 2026 the backlash is complete: most teams that adopted microservices either consolidated back or wish they had. The new default for teams of 5-50 engineers is the modular monolith.
This isn't a return to the bad old days of big-ball-of-mud monoliths. It's a better understanding of what microservices actually cost.
What microservices cost
- Operational complexity. 30 services means 30 deployment pipelines, 30 monitoring setups, 30 dependency updates.
- Network failure modes. What was a function call is now an HTTP call that can fail, time out, be retried, get duplicated.
- Distributed transactions. Forget the ACID database. Now you need sagas, eventual consistency, compensation logic.
- Observability burden. Distributed tracing required to understand a single request flow.
- Coordinating releases. Service A depends on changes to service B, which depends on service C.
- Team friction. Cross-team dependencies for any feature touching multiple services.
What the modular monolith provides
- Single deployable unit. One CI/CD pipeline.
- In-process function calls (fast, reliable).
- Single database, full transaction support.
- Single tracing context.
- Clear module boundaries — code organization that mirrors team boundaries.
- Easy to evolve into microservices later when boundaries are proven and scale demands it.
What "modular" means specifically
Not just "organized code." Specific architectural patterns:
- Module boundaries enforced by code, not convention. Use language features (Java modules, Ruby on Rails engines, Go packages with explicit imports) or build tools (Nx for TypeScript, multi-module Maven/Gradle) to make cross-module imports explicit.
- Module-public interfaces. Each module exposes a defined public API. Internal implementation hidden.
- Module-private data. Each module owns its tables. Other modules access through public API, not direct SQL.
- Module tests at the boundary. Each module independently tested via its public API.
The modular monolith looks like microservices in code organization but deploys as one.
When microservices still make sense
- Truly independent scaling. One component receives 1000x traffic of another and needs its own scaling profile.
- Different programming languages. ML models need Python, real-time service needs Go — actually different runtimes.
- Organizational scale. 50+ engineering teams. Each team needs deployment autonomy.
- Strong isolation requirements. One service handles PII subject to strict regulation, others don't touch it.
For a 5-50 engineer team, none of these usually apply at the start.
Migration: monolith → modular monolith
Easier than monolith → microservices. The pattern:
- Identify natural domain boundaries (orders, billing, identity, catalog).
- Create modules per domain.
- Move code into modules.
- Enforce no cross-module direct calls (use shared interfaces or events).
- Each module gets its own database schema (still in same DB).
- Build per-module tests.
Takes 6-18 months on a substantial codebase. No downtime. Code stays deployable throughout.
Migration: modular monolith → microservices later
If you've maintained module boundaries, splitting one module into a microservice is a contained refactor:
- Public API becomes HTTP/gRPC.
- Database schema migrates to own database.
- Deployment pipeline extracted.
- Observability and monitoring added.
This is much easier than starting microservices and having to fix unclear boundaries later.
Deployment
- Single deployable unit. Container image built from the whole repo.
- Single rolling deploy.
- Feature flags control rollout of new code paths.
- Blue-green or canary if necessary.
- Multiple instances behind a load balancer.
Anti-patterns
- Distributed monolith. Microservices that all must deploy together. Worst of both worlds.
- Premature service extraction. Splitting based on "this might scale" without evidence.
- Microservices for small team. 5 engineers with 12 services — most time spent in YAML.
- Modular monolith with leaky boundaries. Modules reach into each other's data. Same effect as no modules.
What about scaling
A modular monolith scales further than people think. Stack Overflow runs on a small number of servers serving billions of requests. Postgres handles incredible loads with proper tuning. Modern hardware is fast.
Most teams that microserviced for scale could have stayed monolithic for years longer.
Verdict
For teams of 5-50 engineers, the modular monolith is the right default. Single deploy, fast development, easy debugging, room to evolve. Adopt microservices when you have proven boundaries, scaling pressure, language diversity, or 50+ teams — not because they're the modern way.