Patterns: Monolith vs Microservices
Size the architecture to the team — not the trending post
Modular monolith vs microservices is a sizing decision, not a dogma war. The factors: team size/cadence, independence boundaries, ops maturity. Modern middle: modular monoliths with clean seams that can split later, strangler-fig migration, DDD bounded-context discipline.
What you will learn
- Compare monolith/modular/microservices with team-fit lens
- Identify bounded contexts worth separating
- Design for a split later (seams) or a safe strangler migration
Microservices buy you independent deploy + scale + team autonomy, at a tax: observability, networking, data consistency, and ops duplication. A modular monolith buys you target simplicity with DDD seams you can crack later. Choose by team size + cadence + domain boundaries — not by fashion.
What is this?
Shapes: monolith (one deployable; simplest ops; scaling/failure blast = whole app), modular monolith (one deployable, but boundary-encoded modules/PAPI seams — the modern default), microservices (multiple deploys, per-service DB, team-per-context, independent scale). Drivers to micro: independent deploys (cadence spikes), team autonomy (2-pizza per context), independent scale (hot path vs bulk), ownership boundaries. Drivers to monolith: small team, stable domain, rapid iteration, low ops maturity. Hybrid: strangler fig (migrate outwards per capability — classic safe migration), CQRS/event hooks for seams. DDD tells you WHERE the seams are: bounded contexts with explicit service contracts; shared-nothing data. Anti-fail: microservices at 3-person team, or monolith with no boundaries (hard to split later). Observability + test + contracts tooling is the enabler for micro; without it you've split the worst part of your app.
Why do you need it?
- Right-sizing saves the tax: teams that needed micro and stayed mono pay deployment/s competition; teams that split too soon pay ops + observability blow-ups.
- A modular monolith + seams is the 'have both later' structural bet — you keep simplicity today and a real split path.
- Bounded contexts are the honest boundaries: splitting along them is safe; splitting around feelings isn't.
- The strangler pattern makes migration incremental — the cloud way (brownfield) rather than big-bang.
Mental model
A workshop: one bench does everything vs. specialized benches per craft — how many benches fit your crew?
Monolith vs micro
A lone craftsman gains nothing from a dozen specialty benches (it's all transport cost); a big crew bottlenecks at one bench. The bench count (service count) is sized to the crew — and the seams between crafts stay clean either way.
How it works
- Map bounded contexts (DDD): where do data/domain seams naturally live?
- Decide the deployable shape by team size/cadence + context independence needs.
- Keep context boundaries encoded even in a monolith (modules, contracts, no cross-table hacks).
- If micro: per-service DB + contracts + observability from day one; team-per-context.
- Migration: strangler fig — switch one capability at a time behind a facade.
Sizing decision.
Examples
Modular seam in code
# Order domain vs Billing domain as modules with contracts:# module order { export interface OrderServiceFactory }# module billing { export interface BillingGateway }# Keep an anti-corruption layer: order never reads billing tables directly;# it calls the BillingGateway (contract) with its own DTO.# → Today: one deployable (mono), tomorrow: split each module into a service# by moving the contract behind an HTTP call. Seams now, split later.Boundaries are the real wisdom — enforced now, cheaply split later. No ceremony, no micro-tax today.
In production
- Start modular monolith; split only the contexts the team/cadence truly forces (e.g., the nightly-etl hot path).
- Strangler for the legacy migration: capability-by-capability under a facade.
- If true multi-service: per-service DB + OpenAPI/AsyncAPI contracts + full trace/observability + independent deploys.
Best practices
- Size to team + cadence: deploy rhythm and autonomy drive splitting, not the count of services someone admired.
- Encode boundaries even in a monolith (contracts, no cross-context table writes).
- Use a strangler facade for migrations; always incremental.
- Keep data ownership per context — shared DB is how microservices become 'distributed monolith'.
- If splitting: prove it on one context before the fleet-wide party.
Common mistakes
- Microservices because of a slide, at a small team → ops tax + observability debt.
- Monolith with zero boundaries → every split later is a burn while three contexts share tables.
- Splitting the write path across services sharing one DB → distributed transactional corruption.
- Migrating big-bang (rewrite every context at once) instead of strangler.
Debugging tips
- A 'monolith' that fails one sprint → the boundary/contract discipline slipped; search cross-context table access first.
- Microservices await slow deploys yet → their contract/version skew between services; enforce code-gen/versioning.
- Strangler stuck at one capability → the facade did the switch but the old path still serves reads; cutover the read path too.
- Team autonomy promised but tweaks bounce → service ownership/contract-change process undefined; define who breaks what.
Interview checkpoint
Quick quiz
The modern default when the team is small:
A sign you built a 'distributed monolith':