RS
Ravi Shukla
HomeBlogToolsAbout
Resume
RS
Ravi Shukla

Senior Java + AI engineer. Kafka, RAG, distributed systems.

Content

  • Blog
  • System Design
  • AI & ML
  • DevOps

Explore

  • About Ravi
  • Open Stats
  • Thank You

© 2026 Ravi Kant Shukla. All rights reserved.

Deployed on Vercel · Mumbai region

Back to Writing
hld

Monolith vs. Microservices: When (and Why) to Break Up Your Codebase

A pragmatic analysis of monolith and microservices architectures. Explores deployment models, team scaling, operational complexity, and the actual costs of distributed systems for senior engineers making architectural decisions.

May 12, 202618 min read
microservicesarchitecturemonolithsystem-designdistributed-systems

Monolith vs. Microservices: When (and Why) to Break Up Your Codebase

TLDR: Monoliths scale teams better than they scale servers. Microservices scale servers but fragment your team and debugging. The decision isn't technical—it's about organizational structure, deployment velocity, and tolerance for operational complexity.

Architecture Comparison at a Glance


Part 1: The Monolithic Baseline

What Is a Monolith?

A monolith is a single deployable unit:

  • One codebase (or tightly coupled modules)
  • One process (or a handful of instances behind a load balancer)
  • One database (or logically unified data layer)
  • Shared dependencies (same runtime, framework, library versions)

The Monolith Advantages (Why They Last)

1. Operational Simplicity

Timeline: Code → Production

Monolith:   |─ git push ─→ CI (5min) ─→ Deploy (2min) ─→ Live (7min total)

A single docker build && docker push && kubectl apply handles all features. Debugging? You have one process to attach a debugger to, one log stream to grep.

2. Transactional Consistency (ACID)

Money doesn't evaporate. In a monolith, you have database transactions that guarantee atomicity across multiple table writes. Distributed transactions are a nightmare (we'll see this in Part 3).

3. Shared In-Memory State

Latency Comparison:

Monolith (in-memory):
  Request 1: Cache MISS → DB query (5ms)
  Request 2: Cache HIT → same user (0.001ms) ✓ 5000x faster!

Microservices (network):
  Request 1: Fetch from User Service (12ms)
  Request 2: Fetch from User Service (12ms) (no shared cache)

No network calls for cache lookups. Synchronous function calls are nanoseconds, not milliseconds. This matters when you have 10,000 req/s.

4. Team Cohesion

A team of 10 engineers owns the entire product. They share:

  • Deployment schedule (same cadence, same concerns)
  • Debugging expertise (anyone can fix any bug)
  • Knowledge of the system (fewer mental models to maintain)

The Monolith Limitations

1. Scaling the Codebase

Team size   Monolith pain
─────────────────────────
  1-10      Fine
 10-50      Slow CI/CD, merge conflicts
 50-100     Multiple teams stepping on each other
100+        Complete paralysis (everything blocks everything)

At 100+ engineers, every build takes 20 minutes. Every feature touches multiple domains. Code reviews become political.

2. Scaling the Server (Uniform Scaling Problem)

A single process can only use one machine's resources. To scale:

Problem: You can't run auth in-memory cached, but payments on GPU acceleration, and analytics on 500GB of Spark. Everything must fit in the same resource envelope.

3. Risk Concentration

One bug in the payment module takes down the entire site. Single point of failure.

4. Technology Lock-In Java monolith? Every new module is Java. Python? Entire codebase is Python. You can't adopt Go for latency-critical services or Rust for memory safety without a rewrite.


Part 2: The Microservices Promise

What Is a Microservice Architecture?

Multiple independently deployable services:

  • Per-service codebases (different repos, different teams)
  • One service per concern (auth, payments, notifications, analytics)
  • Separate databases (each service owns its data)
  • Network communication (REST, gRPC, events)
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Auth        │     │  Payments    │     │ Notifications│
│  Service     │────→│  Service     │────→│   Service    │
│  (Node.js)   │     │  (Java)      │     │  (Go)        │
└──────────────┘     └──────────────┘     └──────────────┘
   │ PostgreSQL        │ PostgreSQL       │ PostgreSQL
   │ (auth users)      │ (transactions)   │ (webhooks)

Microservices Advantages

1. Independent Deployment

Teams can ship on their own cadence. No waiting for other teams. No deployment windows. No blocking.

Comparison:

MonolithMicroservices
All teams deploy together once/weekAuth deploys 10x/day
Feature A waits for Feature B testsAuth deploys independent of Payments
Hotfix needs approval from allPayments hotfix: 5 min solo deploy

2. Technology Diversity

Auth Service:        Node.js (JWT, OAuth)
Payments Service:    Java (ACID transactions, FX)
Analytics Service:   Python (Pandas, Spark)
Real-time Notif:     Go (goroutines, low latency)

Use the right tool for the job.

3. Independent Scaling

In a monolith, you'd scale all or nothing. Here, you pay for exactly the load.

Cost Comparison:

  • Monolith: Peaks hit auth → scale everything 50x → $12K/month
  • Microservices: Peaks hit payments only → scale payments 50x → $2.4K/month = 80% savings

4. Fault Isolation (Bulkhead Pattern)

Bug in one service doesn't crash others. Users get a graceful fallback while ops team fixes the issue.

5. Team Autonomy Teams own their service end-to-end:

  • Deployment decisions
  • Technology choices
  • Database schema
  • SLA/reliability targets

Part 3: The Microservices Tax (Hidden Costs)

1. The Distributed Systems Tax

What costs nothing in a monolith becomes hard in microservices:

Transactions Across Services

The Problem: Network failures can leave your data in inconsistent states. If Accounts Service debits but Txn Log Service never gets the message, you've lost money from logs.

Monolith (single atomic transaction):

Sql
BEGIN;
  UPDATE account SET balance = balance - 100;
  INSERT INTO transaction_log ...;
COMMIT;  ✓ All or nothing

Microservices (distributed saga):

POST /accounts/debit {amount: 100}  ✓
POST /txn-log/insert {...}         ✗ Network failure
↓
Inconsistent! Need saga pattern + compensating transactions

Call Chain Latency (The Latency Waterfall)

Monolith:  |█| 1ms total
Microservices: |████████████████████████████████████████████████████████████| 63ms total

Microservices is 63x SLOWER because of latency stacking!

Detailed breakdown:

Monolith: User Profile
  ├─ GET /user (in-memory cache, 0.001ms)
  └─ Done: 0.001ms ✓

Microservices: User Profile (Sequential APIs)
  ├─ GET /users/123 (10ms network)
  ├─ GET /payments/user/123 (15ms network + 20ms DB)
  ├─ GET /notifications/user/123 (12ms network + 8ms DB)
  └─ Done: 65ms ❌ (65000x slower!)

This latency matters at scale. At 10K req/s, your servers are blocked waiting for downstream services.

Debugging Distributed Failure

The Visibility Problem:

When a user reports "payment failed," where do you look?

  • Monolith: One log file, grep for the request ID, done in 5 min
  • Microservices: Logs scattered across 20+ services, need distributed tracing setup, takes 30+ min to diagnose, requires additional tools (Jaeger, Datadog, etc.)

2. Operational Complexity Explosion

Container Orchestration (Kubernetes Complexity)

Monolith: 3 containers
  kubectl apply deployment.yaml
  Done!

Microservices: 150 containers
  ├─ Service 1: 10 instances × 7 config items
  ├─ Service 2: 20 instances × 7 config items
  ├─ ...
  ├─ Service 10: 5 instances × 7 config items
  └─ Total: 1,050 configuration options to get right

Example: One missing CPU limit on the Payments Service = OOM kill = cascading failure. Easy to miss in 150 containers.

New Failure Modes

Monolith:
  ├─ Server is down (yes/no)
  └─ Done

Microservices:
  ├─ Service A is down
  ├─ Service B is responding slow (50th percentile: 20ms, 99th: 500ms)
  ├─ Network partition between A and C
  ├─ Database connection pool exhausted
  ├─ Cache coherence broken (A sees v1, B sees v2 of same data)
  ├─ Cascading failure (A→B→C→D all fail in sequence)
  └─ Partial outage (some users affected, others not)

Data Consistency Nightmares

The Root Cause: In microservices, the same logical entity (User) is replicated across databases. Keeping them in sync is your job now, not the database's.

Example scenarios that break:

Scenario 1: User changes email
  Auth Service: email = jane.new@example.com ✓
  Profile Service: still caches email = jane.old@example.com ❌
  
  User gets email notification sent to old address → confusion

Scenario 2: User balance disagreement
  Payments Service: balance = $100 (source of truth)
  Dashboard Service: balance = $95 (cached, 5 min stale)
  
  User sees conflicting numbers → support tickets

Scenario 3: Service outage during sync
  Auth Service deletes user
  Profile Service crashes before getting deletion event
  User is deleted but profile still exists
  Ghost account

3. Organizational Overhead

Communication Tax

Monolith team (10 engineers):
  Daily standup: 15 min
  Design doc review: 1 hour/week
  Done.

Microservices (50 engineers, 10 teams):
  Standup: 30 min (now 50 people)
  API contracts review: 5 hours/week
  Incident response coordination: 10 hours/week
  Dependency mapping meetings: 5 hours/week
  Shared infrastructure discussions: 10 hours/week
  
  That's 30 hours/week lost to coordination.

Onboarding Complexity

Monolith:
  New engineer: "Here's the codebase. Grep for what you need."
  Ramp-up: 2-3 weeks to ship first feature.

Microservices:
  New engineer: "Here's the service inventory (50+ services)."
  Questions:
  - Which service owns user data?
  - How do you test across services?
  - What's the deployment process?
  - How do you debug this failure?
  Ramp-up: 2-3 months to understand the full picture.

Part 4: When to Choose Each

Choose Monolith If:

ConditionWhy
Team size ≤ 30 engineersCoordination overhead isn't worth the decoupling
Single deployment window per weekYou're not shipping frequently enough to justify complexity
No need for tech diversityAll modules use the same stack anyway
Eventual consistency is unacceptableYou need ACID transactions now, not saga patterns
Single-region deploymentNo need to manage cross-region consistency
<1M DAU or <10K req/sYou won't hit scaling limits for years

Example: Early-stage startup (20 engineers), B2B SaaS, deploys weekly. Build a Python + Django monolith. Seriously. Ship fast, iterate with customers, worry about scaling later.

Choose Microservices If:

ConditionWhy
Team size > 50 engineersMonolith becomes a bottleneck for parallel development
Deployment velocity requiredShip multiple times per day across different parts of the system
Tech stack diversity neededAuth in Node, payments in Java, analytics in Python
Fault isolation criticalPayments must never go down even if notifications fail
Extreme scale required>100K req/s, need per-service scaling flexibility
Multi-region deploymentDifferent services needed in different regions

Example: Uber-scale company (500+ engineers), global product, multiple deployment teams. Microservices required for organizational scaling.


Part 5: The Pragmatic Middle Ground

Most successful companies don't pick pure monolith or pure microservices. They start monolithic and carve off services strategically:

Rules for Carving Off Services

Extraction Checklist:

✓ Extract if✗ Don't extract if
10x higher trafficFrequent transactions with core
Different runtime neededShared data model
Separate team owns itSingle team works on it
Crashes must not affect coreUncertain boundaries
Changes 10x more oftenProblem is coordination, not code

The Strangler Fig Pattern (Safe Migration Path)

Named after the strangler fig tree, which grows around an existing tree until the original dies. Apply the same approach to your monolith: grow a new service alongside it, route traffic progressively, and retire the monolith code piece by piece.

Timeline:

Week 0:  Monolith 100%          (baseline, all traffic)
Week 1:  Monolith 100% + Gateway (no traffic change)
Week 2:  Payments extracted, 10% traffic routed to new service
Week 3:  Payments 100%, grow Notifications service (10% traffic)
Week 4:  Notifications 100%, grow Core service rewrite
Week 12: Monolith decommissioned, 5 services live

Total: Zero downtime. Instant rollback if needed.

Why this beats a big-bang rewrite:

  • Zero-downtime migration (traffic is always served)
  • Instant rollback (redirect back to monolith at the gateway — no code changes needed)
  • Test new services in production at small scale before full cutover
  • One domain at a time — mistakes are bounded, not catastrophic

Strangler Fig in practice (Stripe): Stripe extracted its webhook delivery system from the Python monolith this way. They ran the Go webhook service alongside the monolith, gradually routed webhook traffic, and decommissioned the monolith code path over ~6 months — with zero customer-visible incidents.


Part 6: Real-World Case Studies

Case Study 1: Stripe (Monolith → Selective Microservices)

Stripe started monolithic (all Python). As they scaled:

Key insight: Stripe didn't extract everything. They kept the core API monolithic because:

  • It's stable (low change frequency)
  • Doesn't need independent scaling
  • ACID transactions matter
  • Team knows it well

Lesson: Extract only when the pain is real. Stripe's core API is still largely monolithic at scale.

Case Study 2: Amazon (Monolith → Microservices Mandate)

In the early 2000s, Amazon's retail system was a monolith. CEO Jeff Bezos mandated: "All teams must publish their data and functionality through service interfaces."

Result:

  • AWS was born (S3, EC2, etc.)
  • Amazon retail became 100+ independent services
  • Cost: Trillions spent on infrastructure, distributed systems expertise

Lesson: Microservices enabled Amazon's business model (AWS selling infrastructure). Not every company needs this level of decoupling.

Case Study 3: Shopify (Modular Monolith)

Shopify famously kept a large Ruby monolith for years. They scaled via:

  • Careful modularity within the monolith (separate logical concerns)
  • Read replicas for specific queries
  • Extracted only high-risk services (payments, fraud detection)

Lesson: A well-structured monolith scales further than most think. Discipline matters more than architecture.


Part 7: Decision Framework

When facing the monolith vs. microservices decision, answer these questions in order:

Quick Decision Table:

ConstraintAnswerArchitecture
Team size< 30Monolith
30-100Modular monolith + 1-2 services
> 100Microservices
Scaling bottleneckCPU/memoryMonolith (more instances)
Deployment frequencyExtract fast-moving parts
DatabaseExtract to separate DB
ConsistencyACID requiredKeep in monolith
Eventual OKExtract to service
Team structureSingle teamMonolith
Multiple, same appModular monolith
Multiple, different appsMicroservices

Final Comparison Matrix


Conclusion

The dirty truth: Microservices don't make you faster. They make large organizations less slow.

A 5-engineer startup shipping a monolith will outpace a 500-engineer company managing 100 microservices. The startup deploys in 5 minutes. The corporation deploys after 47 approval meetings.

The choice isn't between monolith and microservices. It's between:

  • Slow coordination (monolith, large team)
  • Slow deployment (monolith, large codebase)
  • Slow debugging (microservices, distributed systems)

Pick your poison. Then optimize to reduce the friction.

Start monolithic. Extract services only when the pain of keeping something together exceeds the pain of distributing it. This is the path Stripe, AWS, and every successful company took. Not theoretical architecture, not microservices because it's trendy—pragmatism wins.


Up next: Service Boundaries & Event-Driven Architecture — once you decide to extract services, where exactly do you cut? Why bad service boundaries are worse than no microservices at all, and how Domain-Driven Design tells you where the seams are.


Further Reading

  • Building Microservices by Sam Newman (2nd ed., 2021) — practical boundary decisions
  • Release It! by Michael Nygard — failure modes in distributed systems
  • The Unicorn Project by Gene Kim — organizational scaling and deployment frequency
  • Designing Distributed Systems by Brendan Burns — patterns that work at scale
R

Ravi Kant Shukla

Senior Java + AI engineer. 9+ years in system design, Kafka, microservices, and LLM/RAG pipelines.

About Ravi →More Posts →

Enjoyed this post?

Get more system design and AWS insights delivered weekly. No spam.

Comments (0)

Loading comments...

Leave a comment

Your email will not be displayed publicly.

Related Posts

hld

Database Design for Scalability: SQL, NoSQL, and Polyglot Persistence

Database decisions outlive most code. This post covers relational vs. document vs. key-value vs. time-series vs. graph databases, polyglot persistence, sharding vs. replication, schema evolution, and the decision framework senior engineers use to pick the right store for the job.

database-designsqlnosql
Jun 23, 202624 min read
hld

Consistency & Distributed Transactions: Understanding CAP, ACID, and When to Compromise

Master distributed systems consistency guarantees. Explore ACID vs BASE, CAP theorem nuances, 2PC limitations, consensus algorithms, and when to accept inconsistency. Covers Raft, Paxos, CRDTs, and real-world trade-offs senior engineers face.

distributed-systemsconsistencyconsensus
Jun 12, 202625 min read
hld

API Design at Scale: REST, GraphQL, and gRPC Trade-offs

A practical decision framework for choosing between REST, GraphQL, and gRPC. Covers HTTP semantics, N+1 problems, schema enforcement, streaming, API versioning, rate limiting, and gateway patterns — with real trade-offs senior engineers face.

api-designrestgraphql
Jun 2, 202622 min read