Modular Monolith Over Microservices: A SaaS Architecture Pattern
The default architecture choice for new SaaS in 2018-2022 was "microservices, because Netflix." Most teams that adopted it discovered the operational overhead crushed them. The pendulum has swung. Here's the better default.
Key takeaways
- Modular monolith: single deployable, well-bounded internal modules, shared database.
- Wins: deploy speed, debugging ease, lower operational overhead, faster iteration.
- Loses: independent scaling, polyglot freedom, team autonomy at scale.
- Right default for SaaS until ~50-engineers / extreme scale forces split.
- Split selectively, not preemptively.
Why microservices fail for early SaaS
A 6-engineer team building a SaaS doesn't need 12 microservices. What they get with microservices:
- 12 CI/CD pipelines
- 12 deploys to coordinate for a cross-cutting feature
- 12 sets of observability
- Distributed transactions that are hard to reason about
- Each engineer becomes a part-time devops
The operational tax is real. The benefit (independent scaling, team autonomy) is not yet visible at that team size.
What modular monolith looks like
A single codebase, deployed as one unit (or two if you split web and worker). Inside the codebase:
- Clear module boundaries (e.g.
billing,users,notifications) - Public interfaces between modules (function calls, not HTTP)
- Modules don't directly call each other's database tables
- Each module has its own tests
The discipline is internal architecture, clean boundaries, no cross-module table access, public APIs only. This is the same discipline microservices enforce, without the operational cost.
When the monolith stops working
You feel pain when:
- One team's deploy blocks others
- One module's bug crashes everything
- One module needs to scale to 10x the others
- Different modules need different tech stacks
These signals say "extract this module." Not "rewrite as microservices."
How to extract selectively
When one module's needs diverge:
- Solidify the public interface
- Extract that module to its own deployable
- Replace function calls with HTTP/gRPC calls
- Keep the rest of the monolith intact
You end up with a "small handful" of services (3-7), not 30. This is the realistic end-state for most B2B SaaS.
What we recommend
Start as modular monolith. Maintain the discipline. Extract selectively when a module's needs justify it.
Common pitfalls
Module discipline drift. "Just this once" cross-module DB access becomes the norm. Don't.
Premature extraction. Extracting before pain is operational debt for no benefit.
Monolith that's not modular. A big ball of mud is worse than microservices. Discipline is the prerequisite.
FAQs
Languages? Modular monolith works in Node, Python, Go, Ruby, Java. All have module boundaries.
What about deployment? Single binary or container. Workers separate but still in the same repo.
Multi-region? Multiple regional deploys of the same monolith.
