0x55aa
← Back to Blog

Monolith vs Microservices: Stop Splitting Your App Just Because It's Cool šŸ¢āš”

•16 min read

Monolith vs Microservices: Stop Splitting Your App Just Because It's Cool šŸ¢āš”

Real confession: In 2019, I convinced my team to split our perfectly working monolith into microservices. Why? Because every tech blog, conference talk, and Twitter thread said "microservices are the future!" Six months later, we had:

  • 12 different repos (down from 1)
  • 47 deployment pipelines (up from 1)
  • 3 different databases (was 1)
  • Response times 3x slower (network calls between services!)
  • Developer velocity cut in HALF
  • My team threatening mutiny

My CTO: "Why did we do this again?"

Me: "Because... Netflix does it?"

CTO: "We're not Netflix. We have 10 engineers, not 10,000."

Me, realizing my mistake: 😱

That painful lesson taught me the most important architecture truth: Microservices solve organizational problems, not technical ones. And if you don't have those organizational problems yet, you just created 10 new technical problems for no reason!

The Monolith: Not Dead, Just Misunderstood šŸ¢

Let's start with the truth nobody wants to admit: Monoliths are actually GOOD!

What's a monolith?

Single Application
ā”œā”€ User Management
ā”œā”€ Product Catalog
ā”œā”€ Shopping Cart
ā”œā”€ Checkout
ā”œā”€ Payments
ā”œā”€ Inventory
└─ Analytics

All in ONE codebase
All in ONE deployment
All in ONE database

Why monoliths get a bad reputation:

// People imagine monoliths as this:
// 1 million lines of spaghetti code
// No structure, no modules
// 20-year-old PHP codebase
// Takes 2 hours to deploy
// Crashes if you touch ANYTHING

// But modern monoliths are THIS:
// Clean architecture with modules
// Well-tested, clear boundaries
// Deploys in 2 minutes
// Runs on containers
// Scales horizontally

My Monolith Success Story šŸ’Ŗ

When I was the Technical Lead for an e-commerce startup (2017-2020):

The stack:

// Single Node.js monolith
const app = express();

// Organized by domain modules
app.use('/api/users', require('./modules/users'));
app.use('/api/products', require('./modules/products'));
app.use('/api/orders', require('./modules/orders'));
app.use('/api/payments', require('./modules/payments'));
app.use('/api/inventory', require('./modules/inventory'));

// Single PostgreSQL database
// Single Redis cache
// Single deployment

Scale we achieved:

  • šŸ“Š 10 million users
  • šŸ“ˆ 50,000 orders per day
  • ⚔ Response time: 50-200ms
  • šŸš€ 99.9% uptime
  • šŸ‘„ Team: 8 developers
  • šŸ’° Infrastructure cost: $2,000/month

Deployment:

# One command
git push origin main

# CI/CD pipeline
# → Run tests (2 min)
# → Build Docker image (1 min)
# → Deploy to 5 containers behind load balancer (2 min)
# → Total: 5 minutes from commit to production! ✨

Why it worked so well:

// āœ… No network calls between modules (all in-process!)
app.post('/api/checkout', async (req, res) => {
  // All these are function calls, not HTTP requests!
  const user = await UserService.getUser(req.user.id);      // 1ms
  const cart = await CartService.getCart(req.user.id);      // 2ms
  const inventory = await InventoryService.check(cart);     // 3ms
  const order = await OrderService.create(user, cart);      // 10ms
  const payment = await PaymentService.charge(order);       // 200ms

  res.json({ orderId: order.id });
  // Total: ~220ms āœ…
});

// Compare to microservices:
app.post('/api/checkout', async (req, res) => {
  // Each of these is an HTTP request!
  const user = await fetch('http://user-service/users');        // 50ms + 1ms = 51ms
  const cart = await fetch('http://cart-service/carts');        // 50ms + 2ms = 52ms
  const inventory = await fetch('http://inventory-service');    // 50ms + 3ms = 53ms
  const order = await fetch('http://order-service/orders');     // 50ms + 10ms = 60ms
  const payment = await fetch('http://payment-service/charge'); // 50ms + 200ms = 250ms

  res.json({ orderId: order.id });
  // Total: ~470ms 🐌 (2x slower from network overhead!)
});

A scalability lesson I learned: Network calls are EXPENSIVE! Going from in-process function calls to HTTP requests adds 50-100ms PER CALL. That adds up FAST!

When Microservices Actually Make Sense šŸŽÆ

Microservices solve these REAL problems:

Problem #1: Too Many Developers Fighting Over One Codebase

// Your company BEFORE microservices:
// 100 developers
// All working in same repo
// 50 pull requests open at once
// Merge conflicts CONSTANTLY
// Deploy queue: 20 teams waiting
// One bad deploy breaks everything for everyone

// After microservices:
// Team A owns User Service (10 devs)
// Team B owns Product Service (15 devs)
// Team C owns Order Service (12 devs)
// Each team deploys independently
// No more merge conflict hell!

When I architected systems for a company with 50+ developers, microservices became NECESSARY. Not for scaling - for organizational sanity!

Problem #2: Different Scaling Requirements

// Your application load:
Product Catalog: 10,000 req/sec šŸ”„
Checkout: 500 req/sec
Admin Dashboard: 10 req/sec

// With monolith:
// Have to scale EVERYTHING to handle catalog load
// 20 instances Ɨ $100 = $2,000/month
// 95% of capacity wasted on checkout/admin

// With microservices:
Product Service: 15 instances ($1,500)
Checkout Service: 3 instances ($300)
Admin Service: 1 instance ($100)
// Total: $1,900 (saves money + better resource utilization!)

As a Technical Lead, I've learned: If all your services have similar load patterns, microservices don't help with scaling!

Problem #3: Different Technology Requirements

// Real-world scenario:
// - Main app: Node.js (great for APIs)
// - ML recommendation engine: Python (scikit-learn, TensorFlow)
// - Video processing: Go (fast, efficient)
// - Legacy billing: Java (don't want to rewrite)

// Microservices let you use the right tool for each job!

Problem #4: Need to Deploy Parts Independently

// Your situation:
// - Mobile app releases every 2 weeks
// - Backend needs to support old + new versions
// - Product team ships features daily
// - Payments change once per quarter

// Microservices:
Product Service: Deploy 10x/day āœ…
Payment Service: Deploy 1x/quarter āœ…
Auth Service: Deploy 1x/week āœ…
// Each team moves at their own pace!

When You Should DEFINITELY Stay Monolith šŸ¢

Sign #1: Your Team is Small (< 15 developers)

// With 5-10 developers:
// āŒ DON'T split into 10 microservices
// āœ… DO build a well-structured monolith

// Why?
// - 10 services = 10 repos = 10 deploy pipelines = 10 monitoring setups
// - Context switching between services kills productivity
// - Debugging across services is a nightmare
// - Nobody has the full picture anymore

When designing our e-commerce backend with 8 developers, attempting microservices was a DISASTER. We spent more time on infrastructure than features!

Sign #2: Your App Isn't That Big

// Your codebase:
// 50,000 lines of code
// 200 API endpoints
// 30 database tables

// That's NOT big enough for microservices!
// A well-structured monolith handles this EASILY!

// Real "big" codebases:
// 1,000,000+ lines
// 2,000+ endpoints
// 500+ tables
// 50+ teams
// THAT'S when you need microservices!

Sign #3: You Don't Have DevOps Expertise

// Microservices require:
// āœ… Kubernetes or similar orchestration
// āœ… Service mesh (Istio, Linkerd)
// āœ… Distributed tracing (Jaeger, Zipkin)
// āœ… Centralized logging (ELK, Loki)
// āœ… Service discovery (Consul, Eureka)
// āœ… API Gateway (Kong, AWS ALB)
// āœ… Circuit breakers
// āœ… Distributed monitoring (Prometheus, Grafana)

// If you don't have dedicated DevOps engineers, DON'T DO MICROSERVICES!

A production lesson that saved us: We tried microservices with NO DevOps team. Spent 6 months building infrastructure instead of features. Went back to monolith, shipped 10x faster!

Sign #4: You're Pre-Product-Market Fit

// Startup phase:
// You're trying different features every week
// Pivoting business model
// Not sure what will stick

// Microservices = WRONG choice!
// - Takes 3x longer to build features
// - Changing shared logic is a nightmare
// - You'll throw away 50% of services anyway

// Monolith = RIGHT choice!
// - Ship features fast
// - Easy to refactor
// - Simple to understand
// - Can always split later!

The Hybrid Approach: Modular Monolith šŸŽÆ

The best of both worlds:

// Structure your monolith LIKE microservices!
// Clear module boundaries
// Each module could be extracted if needed

// File structure:
src/
ā”œā”€ā”€ modules/
│   ā”œā”€ā”€ users/
│   │   ā”œā”€ā”€ users.service.js
│   │   ā”œā”€ā”€ users.controller.js
│   │   ā”œā”€ā”€ users.model.js
│   │   └── index.js (exports only the public API)
│   ā”œā”€ā”€ products/
│   │   ā”œā”€ā”€ products.service.js
│   │   ā”œā”€ā”€ products.controller.js
│   │   ā”œā”€ā”€ products.model.js
│   │   └── index.js
│   ā”œā”€ā”€ orders/
│   │   ā”œā”€ā”€ orders.service.js
│   │   ā”œā”€ā”€ orders.controller.js
│   │   ā”œā”€ā”€ orders.model.js
│   │   └── index.js
│   └── payments/
│       ā”œā”€ā”€ payments.service.js
│       ā”œā”€ā”€ payments.controller.js
│       ā”œā”€ā”€ payments.model.js
│       └── index.js
└── app.js

// Key rules:
// 1. Modules ONLY talk through public APIs
// 2. No direct database access across modules
// 3. Shared code in separate package
// 4. Could be extracted to microservice if needed

Example module boundaries:

// users/index.js - Public API
module.exports = {
  getUserById: async (id) => { /* ... */ },
  createUser: async (data) => { /* ... */ },
  updateUser: async (id, data) => { /* ... */ }
};

// orders/orders.service.js - Uses User module correctly
const UserModule = require('../users');

async function createOrder(orderData) {
  // āœ… GOOD: Use public API
  const user = await UserModule.getUserById(orderData.userId);

  // āŒ BAD: Direct database access
  // const user = await db.users.findById(orderData.userId);

  // Create order...
}

Benefits:

// āœ… Simple deployment (monolith)
// āœ… Fast development (no network calls)
// āœ… Easy debugging (all in one process)
// āœ… Clear boundaries (like microservices)
// āœ… Can extract later if needed
// āœ… Best of both worlds! šŸŽ‰

When I designed our e-commerce backend this way, we got 90% of microservices benefits with 10% of the complexity!

The Migration Path: Monolith to Microservices šŸš€

If you MUST split, do it gradually:

Step 1: Start with Strangler Fig Pattern

// Don't rewrite EVERYTHING at once!
// Extract ONE service at a time

// Before:
Monolith [Users, Products, Orders, Payments]

// Phase 1: Extract Payments (most isolated)
Monolith [Users, Products, Orders] ← Payment Service (new!)

// Phase 2: Extract Products
Monolith [Users, Orders] ← Payment Service ← Product Service (new!)

// Phase 3: Continue...
// Takes 12-18 months, but SAFE!

Step 2: Extract by Business Capability

// āœ… GOOD: Extract complete business domains
Payment Service → [Process payment, Refund, Payment history]
Product Service → [Catalog, Search, Inventory, Reviews]

// āŒ BAD: Extract by technical layer
Database Service → [All database calls]
Auth Service → [Just authentication]
// These services are called by EVERYONE = tight coupling!

Step 3: Database-Per-Service (The Hard Part)

// Before (shared database):
Monolith DB
ā”œā”€ users table
ā”œā”€ products table
ā”œā”€ orders table
└─ payments table

// After (separate databases):
User Service → User DB [users table]
Product Service → Product DB [products, inventory]
Order Service → Order DB [orders, order_items]
Payment Service → Payment DB [payments, transactions]

// Challenges:
// - No more JOINs across domains! 😱
// - Need eventual consistency
// - Data duplication
// - Distributed transactions (sagas!)

A scalability lesson that cost us: We tried to split services but keep a shared database. BAD IDEA! Services were coupled through database, defeating the whole purpose!

Step 4: Handle Data Consistency

// Problem: No more ACID transactions across services!

// Before (monolith with transaction):
await db.transaction(async (trx) => {
  await trx('orders').insert(order);
  await trx('payments').insert(payment);
  await trx('inventory').decrement('stock', order.quantity);
  // All succeed or all fail! āœ…
});

// After (microservices - SAGA pattern):
// 1. Create order
const order = await OrderService.create(orderData);

try {
  // 2. Process payment
  const payment = await PaymentService.charge(order.total);

  // 3. Update inventory
  await InventoryService.decrement(order.items);

} catch (error) {
  // Rollback manually!
  await OrderService.cancel(order.id);
  await PaymentService.refund(payment.id);
  // Complex! 😰
}

The Real Costs of Microservices Nobody Talks About šŸ’ø

Cost #1: Infrastructure Complexity

// Monolith:
- 1 load balancer
- 5 app servers
- 1 database
- 1 Redis cache
// Total: ~10 components to manage

// Microservices (12 services):
- 1 API gateway
- 12 services Ɨ 3 instances each = 36 app servers
- 12 databases (or 1 per domain = 4 databases)
- 12 Redis instances
- Service mesh (Istio) across all services
- 1 monitoring stack (Prometheus, Grafana)
- 1 logging stack (ELK)
- 1 tracing system (Jaeger)
// Total: ~80+ components to manage! 😱

// DevOps team required: 2-3 people minimum!

Cost #2: Developer Productivity

// Monolith development:
// Want to add a feature?
// 1. Edit 3 files in same repo
// 2. Write tests
// 3. Deploy
// Time: 2 hours āœ…

// Microservices development:
// Want to add a feature?
// 1. Update User Service (1 hour)
// 2. Update Order Service (1 hour)
// 3. Update API Gateway (30 min)
// 4. Update shared types package (30 min)
// 5. Deploy all 3 services in correct order (1 hour)
// 6. Debug failed integration (2 hours)
// Time: 6 hours 🐌

// Developer velocity: Cut by 50-70%! 😭

Cost #3: Debugging Nightmares

// Monolith debugging:
// User reports bug
// → Set breakpoint
// → Step through code
// → Find bug
// → Fix
// Time to debug: 30 minutes āœ…

// Microservices debugging:
// User reports bug
// → Check API Gateway logs
// → Check Auth Service logs
// → Check Order Service logs
// → Check Payment Service logs
// → Check distributed traces
// → Find bug in network call between services
// → Can't reproduce locally (works on my machine!)
// → Finally fix after trying 5 different things
// Time to debug: 4 hours 😱

When designing our e-commerce backend, I underestimated these costs by 10x! We thought microservices would make us faster. We were SLOWER for 12 months!

Cost #4: Network Failures

// Monolith:
// Function call fails → Exception → Handle it
// Failure rate: 0.001%

// Microservices:
// HTTP call fails → Timeout? Network error? Service down?
// Need: Circuit breakers, retries, fallbacks
// Failure rate: 0.1% (100x higher!)

// Example:
async function checkout(order) {
  try {
    // Each call can fail!
    const user = await UserService.get(order.userId);      // 0.1% fail
    const inventory = await InventoryService.check(items); // 0.1% fail
    const payment = await PaymentService.charge(total);    // 0.1% fail

    // Combined failure rate: ~0.3%
    // 3 in 1000 checkouts fail from network issues! 😱
  } catch (error) {
    // Complex error handling required!
  }
}

The Decision Framework šŸŽÆ

Should you use microservices? Answer these questions:

// Score yourself (1 = No, 5 = Yes):

Team size:
[ ] 1-10 developers → Monolith (99% of the time)
[ ] 10-30 developers → Modular Monolith
[ ] 30-50 developers → Consider Microservices
[ ] 50+ developers → Microservices likely needed

Codebase size:
[ ] < 50k lines → Monolith
[ ] 50k-200k lines → Modular Monolith
[ ] 200k-500k lines → Consider Microservices
[ ] 500k+ lines → Microservices likely needed

Deployment frequency:
[ ] Once per week → Monolith is fine
[ ] Multiple times per day → Modular Monolith
[ ] Different teams need different cadences → Microservices

Technology diversity:
[ ] One primary language → Monolith
[ ] Specific services need different tech → Microservices

DevOps maturity:
[ ] No dedicated DevOps → Monolith
[ ] 1-2 DevOps engineers → Modular Monolith
[ ] Strong DevOps team → Microservices possible

Organizational structure:
[ ] 1-2 teams → Monolith
[ ] 3-5 teams → Modular Monolith
[ ] 5+ teams → Microservices

// If you scored < 15 → MONOLITH
// If you scored 15-25 → MODULAR MONOLITH
// If you scored > 25 → MICROSERVICES

Real-World Examples šŸŒ

Success Stories with Monoliths

Shopify (2024):

  • Largest monolith in the world (3M+ lines of Ruby)
  • Handles millions of merchants
  • Processes billions in transactions
  • Still a monolith! šŸŽ‰

GitHub (2024):

  • Started as monolith, still mostly monolith
  • Extracted a FEW services (CI/CD, Actions)
  • Core platform: One Rails monolith
  • Serves millions of developers! āœ…

Stack Overflow (2024):

  • Monolith serving 100M+ users
  • 9 web servers
  • Blazing fast
  • Proves monoliths can scale! šŸš€

Success Stories with Microservices

Netflix:

  • 700+ microservices
  • 10,000+ engineers
  • Global scale
  • NEEDS microservices! āœ…

Amazon:

  • Thousands of microservices
  • Huge organization
  • Different teams, different technologies
  • Microservices make sense! āœ…

Uber:

  • 2,000+ microservices
  • Complex domains (rides, eats, freight)
  • Global operations
  • Microservices necessary! āœ…

Notice the pattern? All successful microservice companies have THOUSANDS of engineers! Not 10, not 50, THOUSANDS!

The Bottom Line šŸ’”

Microservices aren't better than monoliths - they're DIFFERENT!

The essentials:

  1. Start with a monolith - Always. Every time. No exceptions.
  2. Organize it well - Modular monolith with clear boundaries
  3. Extract services when you HAVE TO - Not when it's trendy
  4. Microservices solve org problems - Not technical problems
  5. Small teams = Monolith - Don't fight this!

The truth about architecture:

There's no "best" architecture - only the RIGHT architecture for YOUR situation! A well-designed monolith beats a poorly-designed microservices mess EVERY TIME!

When designing our e-commerce backend, I learned this: We wasted 6 months and $100K splitting into microservices we didn't need. Went back to a modular monolith. Productivity 2x. Team morale UP. Customers didn't notice or care - they just wanted features!

You don't need microservices from day one. You probably don't need them at day 1000 either. But if you DO eventually need them, a well-structured modular monolith makes extraction easy! šŸš€

Your Action Plan šŸŽÆ

If you're building something new:

  1. Start with a monolith
  2. Use clear module boundaries
  3. Write clean, testable code
  4. Add observability from day one
  5. Scale vertically first, then horizontally

If you're considering microservices:

  1. Ask: "Why?" If answer is "because everyone else does", STOP!
  2. Document current pain points
  3. Calculate complexity costs
  4. Start with ONE extracted service
  5. Evaluate for 3 months before extracting more

If you already have microservices chaos:

  1. Don't panic
  2. Consider consolidating some services
  3. Focus on clear boundaries
  4. Invest in infrastructure
  5. Document service dependencies

Resources Worth Your Time šŸ“š

Reading:

Real talk: The best architecture is the simplest one that solves your actual problems!


Stuck in microservices hell? Connect with me on LinkedIn - I've been there and survived!

Want to see modular monolith patterns? Check out my GitHub - real production architectures!

Now go forth and build monoliths responsibly! šŸ¢āœØ


P.S. If someone tells you "monoliths don't scale", show them Shopify, GitHub, and Stack Overflow. Monoliths scale JUST FINE if you build them right! šŸ“ˆ

P.P.S. I once interviewed at a startup with 3 developers and 15 microservices. They spent 80% of their time on DevOps and infrastructure. The company died 6 months later. Don't be that company! Start simple! šŸ˜