Monolith vs Microservices
When to split and when not to — independent deployability and scaling vs the very real costs of distribution, and how the strangler fig pattern migrates safely.
A monolith packages all functionality into one deployable unit; microservices split it into independently deployable services, each owning a capability and its data. Microservices buy independent deployment, scaling, and team autonomy — but charge you in network calls, distributed data, partial failure, and operational complexity. The right choice depends on scale, team size, and whether clear boundaries exist.
A monolith is a single deployable unit: all modules run in one process and usually share one database. It's the simplest way to start — easy local dev, simple deployment, fast in-process calls, and straightforward transactions across modules. Most successful systems begin here, and many should stay.
Pain drives the split: a huge codebase slows every build and deploy, one module's scaling needs differ wildly from others, teams step on each other, and a bug in one area can crash the whole app. Microservices let teams deploy, scale, and choose technology independently per service.
Splitting trades in-process method calls for network calls — now you face latency, partial failure, retries, distributed transactions, eventual consistency, service discovery, and far more operational complexity. A function call that couldn't fail becomes an RPC that can time out. Distribution is not free.
Don't rewrite — strangle. Put a gateway in front, then extract one capability at a time into a service, routing that traffic to the new service while the monolith handles the rest. Over time the new services 'strangle' the monolith. You ship continuously and can roll back any step.
Key Concepts
One deployable unit, typically one database. Simple to build/run; scaled and deployed as a whole.
An independently deployable, independently scalable service owning its own data and a single capability.
The core benefit: ship one service without redeploying everything else — smaller blast radius.
Incrementally extracting services behind a gateway until the monolith is replaced — no big-bang rewrite.
The worst of both: services that must deploy together and call each other synchronously. An anti-pattern.
A split aligned with a business capability / bounded context, minimizing chatty cross-service calls.
1// The Strangler Fig: extract a service without a big-bang rewrite.2// Route most traffic to the monolith, peel off one capability at a time.34// API gateway / reverse proxy routing:5// /api/search/* → new SearchService (extracted)6// /api/* → legacy Monolith (everything else, for now)78// In code, hide the seam behind an interface the monolith already uses:9interface SearchProvider { query(term: string): Promise<Result[]>; }1011class LegacySearch implements SearchProvider { /* in-process */ }12class RemoteSearch implements SearchProvider { // new microservice13 async query(term: string) {14 return httpClient.get(`${SEARCH_URL}/search?q=${term}`); // network hop15 }16}1718// Flip a feature flag to shift traffic gradually; roll back instantly if needed.19const search: SearchProvider = flags.useRemoteSearch20 ? new RemoteSearch()21 : new LegacySearch();
The monolith-vs-microservices decision shapes a whole system's cost, velocity, and reliability, and it's one of the most common (and most over-simplified) architecture-interview topics. The mature answer isn't 'microservices are better' — it's understanding the trade-offs well enough to start simple and split deliberately when the pain justifies it.
Common Pitfalls
1One Module Needs to Scale 50× More Than the Rest
An e-commerce monolith is fine except during sales, when the search and product-listing load spikes 50× while checkout volume barely changes. Scaling means duplicating the entire app — wasteful and expensive.
In a monolith you scale everything together. Provisioning enough instances to handle search's spikes means paying for 50× of payments, admin, and reporting too — capacity you don't need.
Extract Search (and product catalog) into its own service using the strangler fig pattern. Now that service auto-scales independently to handle spikes, while checkout and the rest stay at a steady, cheap baseline. The split is justified by a real, divergent scaling need.
Takeaway: Extract a service when one part has genuinely different scaling, deployment, or team needs. Independent scalability is a top reason to split — but only for the parts that actually need it.
2The Distributed Monolith Trap
A team eagerly split a small app into 12 microservices. Now every feature touches 5 services that must be deployed together in a specific order, services call each other synchronously in long chains, and a single page load fans out into 20 network hops.
They distributed without finding real boundaries, creating a 'distributed monolith' — all the operational pain of microservices (network, ops, debugging) with none of the independence. Tight synchronous coupling means nothing can deploy or fail alone.
Consolidate over-eager splits back toward a modular monolith, keeping clear module boundaries in one deployable. Extract services later only where a boundary is proven by divergent scaling, team ownership, or release cadence — and prefer async messaging over synchronous call chains.
Takeaway: Microservices are an organizational and scaling tool, not a default. Start with a well-structured (modular) monolith; premature splitting yields a distributed monolith that's harder than either pure option.
Join the discussion
Comments
Loading comments...
Cookie preferences
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.