Microservices vs Monolithic Architecture: When to Use Which
Master microservices vs monolithic architecture for system design interviews. Learn when to use each with real examples from Netflix, Uber, Amazon, and common migration patterns

Building a House vs Building a City
Imagine you're building a house:
- One blueprint
- One foundation
- One roof
- Everything connected
- Fix the plumbing? Access the whole structure.
This is a monolith.
Now imagine building a city:
- Independent neighborhoods
- Separate water systems
- Individual power grids
- Fix one neighborhood's plumbing? Others unaffected.
This is microservices.
Both architectures work. The question is: Are you building a house, or a city?
In this guide, I'll break down monolithic vs microservices architecture, when to use each, how companies like Netflix and Amazon made the transition, and the pitfalls to avoid. Let's dive in.
What Is Monolithic Architecture?
Monolithic = All code in a single, unified application
Structure:
βββββββββββββββββββββββββββββββ
β Monolithic Application β
β β
β βββββββββββββββββββββββββ β
β β User Management β β
β βββββββββββββββββββββββββ β
β βββββββββββββββββββββββββ β
β β Product Catalog β β
β βββββββββββββββββββββββββ β
β βββββββββββββββββββββββββ β
β β Order Processing β β
β βββββββββββββββββββββββββ β
β βββββββββββββββββββββββββ β
β β Payment Service β β
β βββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββ
β
[Single Database]
All features:
- Deployed together
- Share same codebase
- Run in same process
- Access same database
Example: E-Commerce Monolith
Single application:
# app.py
from flask import Flask, request
app = Flask(__name__)
# User management
@app.route('/users', methods=['POST'])
def create_user():
# Insert user into database
return {'status': 'user created'}
# Product catalog
@app.route('/products', methods=['GET'])
def get_products():
# Fetch products from database
return {'products': [...]}
# Order processing
@app.route('/orders', methods=['POST'])
def create_order():
# Create order, reduce inventory, process payment
return {'status': 'order placed'}
# Payment processing
@app.route('/payments', methods=['POST'])
def process_payment():
# Charge credit card
return {'status': 'payment processed'}
if __name__ == '__main__':
app.run()
Everything in one file (or one project), deployed as single unit.
Advantages of Monolithic Architecture
β 1. Simple to Develop
Start coding immediately:
$ git clone repo
$ pip install requirements.txt
$ python app.py
# Application running!
No complex infrastructure, no service orchestration.
β 2. Easy to Test
One application = one test suite:
$ pytest
# Test entire application
No need to mock 20 microservices.
β 3. Simple to Deploy
$ docker build -t myapp .
$ docker push myapp
$ kubectl apply -f deployment.yaml
# Done!
One deployment, not 50.
β 4. Better Performance (Initially)
Monolith:
# Get user and their orders (in-memory)
user = db.query("SELECT * FROM users WHERE id = 1")
orders = db.query("SELECT * FROM orders WHERE user_id = 1")
# Total: 2 database queries, < 50ms
Microservices:
# Get user (network call to User Service)
user = requests.get("http://user-service/users/1") # 20ms
# Get orders (network call to Order Service)
orders = requests.get("http://order-service/orders?user_id=1") # 20ms
# Total: 2 network calls, > 40ms (plus network overhead)
β 5. Easier to Debug
Single codebase:
- Stack traces show full flow
- Logs in one place
- One application to monitor
Disadvantages of Monolithic Architecture
β 1. Tight Coupling
Problem: Everything depends on everything
# In monolith
def create_order(user_id, product_id):
user = get_user(user_id) # User module
product = get_product(product_id) # Product module
if product.inventory > 0:
reduce_inventory(product_id) # Inventory module
charge_payment(user.payment_method) # Payment module
send_email(user.email) # Email module
return order
Change email module? β Test entire order flow (everything's connected)
β 2. Hard to Scale
Scenario: Only payment processing is slow
Monolith solution: Scale entire application
Before: 1 instance
After: 10 instances (all 10 include user, product, order, payment)
Problem: Wasting resources on unused modules
Can't scale just payment service.
β 3. Slow Development
100 developers working on monolith:
- Merge conflicts (everyone editing same codebase)
- Long build times (compile entire application)
- Testing takes hours (integration tests for everything)
- Deploy rarely (risk breaking everything)
Real Example: Amazon in 2001
- One monolithic codebase
- Deploys every 11.6 seconds caused cascading failures
- Teams blocked by each other's changes
β 4. Technology Lock-In
Monolith in Java:
- All features must be Java
- Want to use Python for ML model? Can't easily integrate
- Stuck with Java version (upgrading risky)
β 5. Single Point of Failure
Bug in payment module:
Payment module crashes
β
Entire application crashes
β
Users can't browse products (even though product module works!)
What Is Microservices Architecture?
Microservices = Application split into small, independent services
Structure:
[User Service] [Product Service]
β β
[User DB] [Product DB]
[Order Service] [Payment Service]
β β
[Order DB] [Payment DB]
Each service:
- Independent codebase
- Own database
- Deployed separately
- Communicates via APIs (HTTP, gRPC, message queues)
Example: E-Commerce Microservices
User Service:
# user_service.py
@app.route('/users', methods=['POST'])
def create_user():
# Insert into user database
return {'status': 'user created'}
Product Service:
# product_service.py
@app.route('/products', methods=['GET'])
def get_products():
# Fetch from product database
return {'products': [...]}
Order Service:
# order_service.py
@app.route('/orders', methods=['POST'])
def create_order():
# Call User Service
user = requests.get(f"{USER_SERVICE_URL}/users/{user_id}")
# Call Product Service
product = requests.get(f"{PRODUCT_SERVICE_URL}/products/{product_id}")
# Call Payment Service
payment = requests.post(f"{PAYMENT_SERVICE_URL}/payments", json={...})
# Create order
return {'status': 'order placed'}
Each service:
- Separate codebase
- Separate repository
- Separate deployment
Advantages of Microservices
β 1. Independent Scaling
Problem: Payment processing needs more resources
Microservices solution: Scale only Payment Service
Before:
- Payment Service: 2 instances
- User Service: 2 instances
- Product Service: 2 instances
After:
- Payment Service: 10 instances β Scaled
- User Service: 2 instances
- Product Service: 2 instances
Cost: Only 8 additional instances (not entire app)
β 2. Technology Flexibility
Different services, different languages:
User Service: Python (Flask)
Product Service: Node.js (Express)
Payment Service: Go (high performance)
Recommendation Service: Python (scikit-learn)
Image Processing: Python (OpenCV)
Choose the best tool for each job.
β 3. Faster Development
100 developers, 10 teams:
- Team 1: User Service (own repo)
- Team 2: Product Service (own repo)
- ...
- Team 10: Payment Service (own repo)
Benefits:
- No merge conflicts between teams
- Deploy independently (100+ deploys/day possible)
- Faster iteration
Real Example: Netflix
- 700+ microservices
- Deploys thousands of times per day
- Teams move independently
β 4. Fault Isolation
Payment Service crashes:
Payment Service down β
β
Order placement fails β
β
But users can still:
- Browse products β
- View their profile β
- Add to cart β
(Graceful degradation: Show "Payment temporarily unavailable")
Compare to monolith: Everything crashes.
β 5. Easier to Maintain
Small services:
- User Service: 5,000 lines of code (understandable)
- vs Monolith: 500,000 lines (cognitive overload)
New developer:
- Monolith: 2 months to understand
- Single microservice: 1 week
Disadvantages of Microservices
β 1. Increased Complexity
Monolith:
1 application
1 database
1 deployment
Microservices:
50 services
50 databases
50 deployments
Service discovery
Load balancers
API gateways
Message queues
Monitoring (50 services!)
Distributed tracing
Operational overhead is HUGE.
β 2. Network Latency
Monolith (in-process):
def get_user_orders(user_id):
user = get_user(user_id) # In-memory call (< 1ms)
orders = get_orders(user_id) # In-memory call (< 1ms)
# Total: 2ms
Microservices (network calls):
def get_user_orders(user_id):
user = requests.get(f"{USER_SERVICE}/users/{user_id}") # 20ms
orders = requests.get(f"{ORDER_SERVICE}/orders?user_id={user_id}") # 20ms
# Total: 40ms (20x slower!)
β 3. Data Consistency Challenges
Scenario: Create order (requires updating inventory and creating payment)
Monolith (transactions):
def create_order():
db.begin_transaction()
try:
reduce_inventory() # Same database
create_payment() # Same database
db.commit() # Atomic!
except:
db.rollback() # All or nothing
Microservices (distributed transaction):
def create_order():
# Call Inventory Service
requests.post(f"{INVENTORY_SERVICE}/reduce", ...) # Success
# Call Payment Service
requests.post(f"{PAYMENT_SERVICE}/charge", ...) # Fails! β
# Now what? Inventory already reduced, payment failed
# Need: Saga pattern, event sourcing, or compensating transactions
Much harder.
β 4. Testing Complexity
Monolith:
$ pytest # Test entire app
Microservices:
# Need to test:
1. Individual services (unit tests)
2. Service interactions (integration tests)
3. End-to-end flows (E2E tests)
# Requires:
- Mock services
- Test environments for all services
- Contract testing
β 5. Debugging Difficulty
Monolith error:
Stack trace shows:
File "app.py", line 45, in create_order
File "payment.py", line 23, in charge_payment
Clear path: app.py β payment.py
Microservices error:
Order Service β Calls Payment Service β Calls Fraud Detection Service β Fails
Logs scattered across 3 services
Need distributed tracing (Jaeger, Zipkin)
When to Use Monolithic Architecture
β 1. Starting a New Project
Early-stage startup:
- Small team (< 10 developers)
- Uncertain requirements
- Need to ship fast
Start monolith, split later if needed.
Real Example: Airbnb, Twitter, Shopify all started as monoliths.
β 2. Small Applications
Internal tools:
- Admin dashboards
- Employee directory
- Simple CRUD apps
No need for microservices complexity.
β 3. Tight Budget / Small Team
Microservices require:
- DevOps expertise
- Infrastructure (Kubernetes, service mesh)
- Monitoring tools
If you don't have resources, stick with monolith.
When to Use Microservices
β 1. Large Teams (50+ Developers)
Netflix:
- 1,000+ developers
- Impossible to coordinate in single codebase
- Microservices enable team autonomy
β 2. Need Independent Scaling
Uber:
- Ride matching service: High traffic, needs 100+ instances
- Driver payout service: Low traffic, needs 2 instances
Microservices save costs (don't scale everything).
β 3. Different Technology Requirements
Real Example: Spotify
- Recommendation engine: Python (TensorFlow)
- Real-time streaming: C++ (performance)
- Web API: Node.js (async I/O)
Microservices allow using best tool for each job.
β 4. Frequent Deployments
Amazon:
- Deploys every 11.6 seconds (used to be 11.6 seconds, now even faster)
- Only possible with microservices (deploy one service at a time)
Migration: Monolith to Microservices
The Strangler Fig Pattern
Don't rewrite from scratch! Gradually extract services.
Steps:
1. Identify independent module (e.g., Payment)
2. Extract into separate service
3. Route traffic to new service
4. Keep monolith running
5. Repeat for next module
Example: Uber
2010: Monolithic Python app
Migration:
Year 1: Extract dispatch service β Microservice
Year 2: Extract payment service β Microservice
Year 3: Extract driver tracking β Microservice
...
Year 5: Monolith mostly gone (legacy code only)
By 2020: Over 2,000 microservices.
Don't Split Too Early
Common mistake: "Let's start with microservices!"
Problem:
- Unknown domain boundaries (what should be a service?)
- Over-engineering (3 users don't need 50 microservices)
- Development slows down (complex infrastructure)
Better: Start monolith, split when pain points emerge.
Signals to split:
- Team > 20 developers
- Deployment takes > 1 hour
- Frequent merge conflicts
- Hard to scale specific features
Hybrid Approach: Modular Monolith
Best of both worlds:
Structure:
Monolithic Application
βββ User Module (clear boundaries)
βββ Product Module (independent)
βββ Order Module (loosely coupled)
βββ Payment Module (can be extracted later)
Characteristics:
- Single deployment
- But clear module boundaries
- Easy to extract into microservice later
Real Example: Shopify
Shopify uses modular monolith:
- Fast development
- Clear boundaries
- Can extract services when needed
Decision Matrix
| Factor | Monolith | Microservices |
|---|---|---|
| Team Size | < 20 developers | 50+ developers |
| Maturity | Early stage | Mature product |
| Scalability Need | Uniform | Non-uniform |
| Deployment Frequency | Weekly | Daily |
| Complexity Tolerance | Low | High |
| Budget | Tight | Flexible |
System Design Interview Tips
Common Question: "Design Uber"
Answer:
Start with context:
"Initially, Uber was a monolith. As they scaled, they migrated to microservices. I'll design the microservices version."
Microservices breakdown:
1. User Service (riders, drivers)
2. Ride Matching Service (match rider to driver)
3. Trip Service (ongoing trips)
4. Payment Service (process payments)
5. Notification Service (push notifications)
6. Pricing Service (surge pricing)
7. Map Service (routing, ETA)
Why microservices for Uber?
β
Independent scaling (ride matching needs 100x more capacity than payment)
β
Large team (1,000+ engineers)
β
Frequent deployments (ship features independently)
β
Fault isolation (map service down β payment down)
What to Mention
β Start by clarifying team size, scale, and requirements β Mention trade-offs (complexity vs autonomy) β For small scale: Suggest monolith β For large scale: Suggest microservices with clear boundaries β Mention communication (REST, gRPC, message queues) β Discuss data consistency (eventual consistency, sagas)
Avoid These Mistakes
β Saying "microservices are always better" (context matters!) β Not mentioning disadvantages (shows lack of depth) β Over-engineering (5 users don't need 50 microservices) β Not explaining service boundaries (how did you split?)
Conclusion
Monolithic vs Microservices isn't "better" or "worse"βit depends on context:
Monolith:
- Simple, fast to develop
- Great for small teams, early-stage products
- Lower operational overhead
Microservices:
- Complex, but enables team autonomy
- Necessary for large teams, high scale
- Expensive to operate
The truth? Most successful companies started with monoliths and migrated to microservices as they scaled.
Your path:
- Start monolith (or modular monolith)
- Grow your team and product
- Extract services when pain points emerge
- Gradually migrate (strangler fig pattern)
Don't prematurely optimize. Build a house first. When it grows into a city, you'll know when to split.