Covers: Monolith vs Microservices Trade-offs, Strangler Fig Pattern, Distributed Monolith Anti-pattern, Domain-Driven Design
The Pivot That Took Amazon 4 Years
In 2001, Amazon's entire website was a single, massive application — a monolith. Every feature shared one codebase, one database, one deployment process. Adding a new feature meant touching code that thousands of other features also depended on. A bug in the recommendations engine could crash checkout.
Amazon's leadership made a decision that, at the time, seemed almost insane: break the monolith into hundreds of independently deployable services, each owned by a small team, each with its own database, communicating only through APIs.
It took roughly 4 years. It required organizational restructuring — Jeff Bezos's famous "two-pizza team" mandate (every team small enough to be fed by two pizzas) and the equally famous API mandate (every team must expose its functionality through an API, with no backdoor database access).
The result became the architectural foundation for AWS itself — many AWS services originated as internal tools Amazon built to manage this transition.
But here's what most engineers miss about this story: Amazon ran as a successful monolith for years before this migration. The monolith wasn't a mistake — it was the right architecture for that stage. The migration happened because their scale and organizational structure had outgrown it.
This is the lens through which every "microservices vs monolith" decision should be made.
The Monolith: Simple, Until It Isn't
A monolith is a single deployable unit containing all application logic — UI, business logic, data access — typically backed by one database.
┌─────────────────────────────────┐
│ Monolith App │
│ ┌─────────┐ ┌─────────┐ │
│ │ Users │ │ Orders │ │
│ ├─────────┤ ├─────────┤ │
│ │ Payment │ │ Inventory│ │
│ └─────────┘ └─────────┘ │
│ (single codebase) │
└─────────────────────────────────┘
│
▼
┌─────────────┐
│ Database │
└─────────────┘
Why Monoliths Are Genuinely Good (Not Just "For Beginners")
Simplicity of development:
One codebase. One IDE project. git clone, run, develop. No need to run 15 services locally to test a feature.
Simplicity of deployment:
One artifact to build, test, and deploy. One CI/CD pipeline. One thing to monitor.
Transaction integrity:
A single database means ACID transactions across your entire data model. Updating a user's order and their loyalty points balance? One transaction. Done.
Performance:
Function calls within a monolith are nanoseconds. Calls between microservices involve network round-trips — milliseconds. For tightly coupled operations, a monolith is faster.
Easier debugging:
A single stack trace shows you the entire request path. No distributed tracing required.
Where Monoliths Genuinely Struggle
One bug crashes everything:
A memory leak in the reporting module can bring down checkout, because they share the same process.
Scaling is all-or-nothing:
If your image processing is CPU-heavy but your API is I/O-bound, you can't scale them independently. You scale the entire monolith — wasting resources on the parts that didn't need it.
Deployment risk increases with size:
Every deploy ships everything — even unrelated changes. A small CSS fix and a major database migration go out together. Larger blast radius for every release.
Team coordination overhead:
As teams grow, multiple teams modifying the same codebase create merge conflicts, deployment queues, and "whose change broke production" debates.
Microservices: Independent, Until They're Not
Microservices split the application into independently deployable services, each owning its own data, communicating over the network.
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Users │ │ Orders │ │ Payment │ │Inventory │
│ Service │ │ Service │ │ Service │ │ Service │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
▼ ▼ ▼ ▼
[Users DB] [Orders DB] [Payment DB] [Inventory DB]
Why Microservices Are Genuinely Good
Independent deployment:
The Payment team can deploy 10 times a day without coordinating with the Inventory team. Smaller, more frequent, lower-risk deploys.
Independent scaling:
If Inventory Service needs 50 instances during a flash sale but Payment only needs 5, you scale them separately. No wasted resources.
Technology diversity:
The Recommendation Service can be Python with ML libraries. The Payment Service can be Java for its mature ecosystem. The real-time Notification Service can be Go for concurrency. Each team picks the right tool.
Fault isolation:
If the Recommendation Service crashes, users can still browse, add to cart, and checkout. The blast radius of a failure is contained — if you've designed fault tolerance correctly (Topic 18).
Team autonomy:
Teams own their services end-to-end — code, database, deployment, on-call. This is the organizational benefit that often matters more than the technical one.
Where Microservices Genuinely Struggle
Network overhead and latency:
What was a function call is now an HTTP/gRPC call — milliseconds instead of nanoseconds. A user-facing request that touches 10 services has 10x the network hops, each adding latency (remember Day 2's tail latency lesson).
Distributed transactions are hard:
No more BEGIN TRANSACTION across services. You need Sagas (Day 5) for anything that spans services.
Operational complexity explodes:
Instead of monitoring one application, you monitor dozens — each with its own logs, metrics, deployment pipeline, and on-call rotation. You need service discovery, API gateways, distributed tracing — infrastructure that doesn't exist in a monolith.
Testing is harder:
Integration testing requires spinning up multiple services (or sophisticated mocking). "Works on my machine" becomes much harder to achieve.
The cost is real and upfront. The benefits compound over time — which is why the decision depends heavily on your current scale and trajectory.
The Strangler Fig Pattern: How to Actually Migrate
Named after the strangler fig tree, which grows around a host tree, gradually replacing it while the host continues to live — until eventually the host is gone and only the fig remains.
This is the pattern for migrating monolith to microservices without a risky "big rewrite."
Step 1: Monolith handles everything
┌─────────────────────────────┐
│ Monolith │
│ [Users][Orders][Payment]... │
└─────────────────────────────┘
↑
All traffic
Step 2: Introduce a proxy/router in front
┌─────────┐ ┌─────────────────────────────┐
│ Proxy │ → │ Monolith │
└─────────┘ └─────────────────────────────┘
All traffic still routes to monolith
Step 3: Extract ONE service. Proxy routes its traffic there.
┌─────────┐ ┌──────────────────────────┐
│ Proxy │ → │ Monolith (minus Users) │
│ │ └──────────────────────────┘
│ │ → ┌──────────────┐
└─────────┘ │ Users Service │
└──────────────┘
/users/* → Users Service
everything else → Monolith
Step 4: Repeat for each module, one at a time
Step 5: Eventually, monolith handles nothing — it's fully "strangled"
Why this works:
- Each extraction is small and low-risk — you can stop the migration at any point with a working system
- The proxy means clients never notice the migration happening
- You learn from each extraction and apply lessons to the next
- If an extraction goes badly, route traffic back to the monolith (rollback is trivial)
Real-world timeline: This is genuinely slow. Amazon's migration took years. Shopify's modularization effort (moving toward a "modular monolith" — a middle ground) has been multi-year. Anyone promising a "quick microservices migration" is underestimating the work.
The Distributed Monolith: Worst of Both Worlds
This is the anti-pattern that catches teams who adopt microservices without understanding why the patterns exist.
Symptoms of a distributed monolith:
❌ Services share a database
→ "Microservices" that all read/write the same tables
→ A schema change in one service breaks three others
❌ Services must be deployed together
→ Service A's new API requires Service B to deploy simultaneously
→ You've recreated a monolith's deployment coupling, but now over a network
❌ Synchronous call chains for everything
→ Service A calls B calls C calls D, synchronously, for every request
→ One slow service makes everything slow (no isolation benefit)
→ If any service is down, the whole chain fails
❌ Shared libraries with business logic
→ A "common" library contains business rules used by every service
→ Changing the library requires redeploying every service that uses it
The result: You have all the operational complexity of microservices (network calls, distributed tracing, multiple deployments, service discovery) — with none of the benefits (no independent deployment, no fault isolation, no independent scaling).
How to avoid it:
- Each service owns its data — no shared databases, ever
- Use async communication (events, queues) for cross-service workflows where possible
- Version your APIs so services can deploy independently
- Duplicate small amounts of logic rather than sharing libraries with business rules — "a little duplication is far cheaper than a tight coupling"
Domain-Driven Design: How to Draw Service Boundaries
The hardest question in microservices isn't "should we split?" — it's "where do we split?"
Domain-Driven Design (DDD) provides a framework: identify bounded contexts — areas of the business with their own language, rules, and models.
E-commerce domain, split into bounded contexts:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Catalog Context │ │ Ordering Context │ │ Fulfillment Context│
│ │ │ │ │ │
│ "Product" means: │ │ "Product" means: │ │ "Product" means: │
│ - name, images │ │ - SKU, price, │ │ - dimensions, │
│ - description │ │ quantity │ │ weight │
│ - category │ │ - in an order │ │ - warehouse │
│ │ │ │ │ location │
└─────────────────┘ └─────────────────┘ └─────────────────┘
The key insight: "Product" means something different in each context. The Catalog team thinks about marketing content. The Ordering team thinks about price and inventory. The Fulfillment team thinks about physical dimensions and warehouse logistics.
Trying to have one unified "Product" model serving all three contexts leads to a bloated, constantly-changing entity that every team fights over. DDD says: let each context have its own model of "Product." Translate between them at the boundaries (via events or API calls).
This maps directly to service boundaries:
Catalog Service → owns "Product" (marketing view)
Ordering Service → owns "OrderLineItem" (price/quantity view)
Fulfillment Service → owns "ShippableItem" (logistics view)
Each service has a clean, focused model. They communicate via well-defined contracts (events: ProductCreated, OrderPlaced, ItemShipped).
The practical exercise: Get your domain experts (not just engineers) in a room and map out the "ubiquitous language" of each part of the business. Where the vocabulary changes meaning, that's a likely service boundary.
Nanoservices: The Opposite Extreme
If microservices done wrong gives you a distributed monolith, microservices done too far gives you nanoservices — services so small that the overhead of running them exceeds the value they provide.
❌ Nanoservice anti-pattern:
- "GetUserNameService" — does one thing: returns a user's name
- "GetUserEmailService" — does one thing: returns a user's email
- "GetUserPhoneService" — does one thing: returns a user's phone
To render a user profile, the client now makes 3 network calls
for data that lives in the same database row.
The rule of thumb: A service should encapsulate a meaningful business capability — not a single field, not a single function. If two pieces of data are always read together and always change together, they probably belong in the same service.
When NOT to Use Microservices
This question is asked constantly in interviews, and the honest answer matters:
Don't use microservices when:
Your team is small (< 10-15 engineers). The operational overhead of microservices requires dedicated platform/DevOps investment that small teams can't afford. A modular monolith gives you most organizational benefits without the operational tax.
Your domain isn't well understood yet. If you're still discovering your product (early startup, MVP), service boundaries drawn too early will be wrong boundaries — and changing service boundaries is far more expensive than changing module boundaries within a monolith.
You don't have strong DevOps/platform capabilities. Microservices require CI/CD per service, service discovery, distributed tracing, centralized logging. Without this infrastructure, you'll spend more time fighting infrastructure than building product.
Your transactions are heavily relational. If most operations touch many entities in ACID transactions (financial ledgers, inventory systems with strict consistency), splitting them across services forces you into complex sagas for what used to be a single
COMMIT.
The honest industry trend (2023-2026): Many companies that adopted microservices early are now consolidating into "modular monoliths" — single deployments with strong internal module boundaries, ready to extract services later if needed, but without the network overhead until it's justified. Shopify, Amazon (for some services), and many others have walked back overly granular microservices.
Interview Scenario: "How Would You Split a Monolith?"
The structured answer:
"First, I'd map the domain using DDD — identify bounded contexts where the business vocabulary and rules genuinely differ. I wouldn't start with technical boundaries (database tables); I'd start with business capabilities.
Then I'd use the Strangler Fig pattern — introduce a proxy/gateway, and extract one service at a time, starting with the module that has the clearest boundary and least coupling to others. I'd pick something with low risk first to validate the approach before tackling the complex, tightly-coupled modules.
Critically, each extracted service gets its own database from day one — no shared tables, even temporarily, because that's how distributed monoliths happen. Cross-service consistency needs would be handled with sagas or eventual consistency via events, not distributed transactions.
I'd also resist over-extraction — if two pieces of functionality are always read and written together, they probably belong in the same service even if they're conceptually 'different things.'"
Key Takeaways
- Monoliths are simple to develop, deploy, and debug — and genuinely the right choice for small teams and early-stage products.
- Microservices enable independent deployment, scaling, and team autonomy — at the cost of network overhead, operational complexity, and harder transactions.
- Strangler Fig pattern: migrate incrementally via a proxy, extracting one service at a time — never a "big rewrite."
- Distributed monolith: the anti-pattern where you get microservices' complexity without their benefits — usually caused by shared databases or synchronous coupling.
- Domain-Driven Design helps draw service boundaries around bounded contexts — areas where business vocabulary and rules genuinely differ.
- Nanoservices: services so granular the network overhead exceeds their value. A service should encapsulate a business capability, not a field.
- The industry is increasingly favoring modular monoliths as a middle ground — strong internal boundaries, single deployment, services extracted only when justified by genuine scaling or team-autonomy needs.
What's Next
Topic 17 covers Service Discovery and Service Mesh — once you have many services, how do they find each other, and how does infrastructure like Istio handle retries, mTLS, and circuit breaking transparently?
Tags: system-design microservices software-architecture backend domain-driven-design distributed-systems interview-prep
Top comments (0)