Architecture

Microservices vs Monoliths: A Practical Guide

February 5, 2025
6 min read

After building and maintaining both monolithic applications and microservices architectures at scale, I've learned that the "microservices vs monolith" debate misses the point. The right choice depends entirely on your context.

The Monolith Myth

Let's dispel some myths:

  • Myth: Monoliths can't scale

  • Reality: Many successful companies run monoliths serving billions of requests

  • Myth: Microservices are always more scalable

  • Reality: Microservices add complexity that can hinder scaling if not managed properly

When Monoliths Make Sense

Start with a monolith if:

1. You're a Small Team

  • Fewer than 10 developers
  • Everyone can understand the full codebase
  • Coordination overhead would be significant

2. Your Domain is Unclear

  • Product-market fit not proven
  • Rapid iteration needed
  • Unclear how to split services

3. Simple Deployment Needs

  • Don't need independent scaling
  • Uniform resource requirements
  • Simple deployment pipeline is sufficient

When to Consider Microservices

Move to microservices when:

1. Team Growth

  • Multiple teams working on same codebase
  • Merge conflicts and coordination overhead increasing
  • Teams blocked waiting for others

2. Scaling Requirements

  • Different parts need different scaling strategies
  • Some components need GPU, others just CPU
  • Traffic patterns vary significantly by feature

3. Technology Diversity

  • Some problems better solved with different tech stacks
  • Legacy systems need gradual migration
  • Team expertise varies by component

The Modular Monolith

Often the best of both worlds:

monolith/
├── modules/
│   ├── auth/         # Could become a microservice
│   ├── users/
│   ├── orders/
│   └── billing/
├── shared/
└── api/

Benefits:

  • Easy to split later if needed
  • Simple deployment and debugging
  • No network overhead between modules
  • Enforced boundaries within codebase

Migration Strategy

If you must migrate, do it gradually:

Phase 1: Identify Boundaries

# Current monolith
[Monolith]
  ├── User Management
  ├── Orders
  ├── Payments
  └── Notifications

Phase 2: Extract Strangler

[Monolith]           [Notification Service]
  ├── Users    ←→    └── Send notifications
  ├── Orders
  └── Payments

Phase 3: Continue Extraction

[Monolith]     [Notifications]  [Payments]
  ├── Users    
  └── Orders

Architecture Patterns

API Gateway

// Gateway routes requests to appropriate services
class APIGateway {
  async handleRequest(req: Request) {
    const path = req.path;
    
    if (path.startsWith('/users')) {
      return this.userService.handle(req);
    }
    if (path.startsWith('/orders')) {
      return this.orderService.handle(req);
    }
    // ... more routes
  }
}

Service Communication

Synchronous (REST/gRPC):

  • Good for: Real-time requirements, simple workflows
  • Bad for: Long-running operations, high coupling

Asynchronous (Message Queue):

  • Good for: Decoupling, resilience, event-driven
  • Bad for: Debugging, consistency challenges

Data Management

Each service owns its data:

[User Service]
  └── Users DB

[Order Service]
  └── Orders DB
  
# Don't access other service's DB directly!
# Use APIs or events instead

Operational Complexity

Microservices require:

Observability

// Distributed tracing
import { trace } from '@opentelemetry/api';

async function handleOrder(orderId: string) {
  const span = trace.getTracer('orders').startSpan('process-order');
  
  try {
    await this.validateOrder(orderId);
    await this.processPayment(orderId);
    await this.sendConfirmation(orderId);
    
    span.setStatus({ code: SpanStatusCode.OK });
  } catch (error) {
    span.setStatus({ code: SpanStatusCode.ERROR });
    throw error;
  } finally {
    span.end();
  }
}

Service Discovery

  • Kubernetes with service names
  • Consul or similar
  • Cloud-native solutions (AWS ECS Service Discovery)

Circuit Breakers

class CircuitBreaker {
  private failures = 0;
  private lastFailTime?: Date;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  
  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (this.shouldAttemptReset()) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is open');
      }
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailTime = new Date();
    
    if (this.failures >= 5) {
      this.state = 'open';
    }
  }
}

Cost Analysis

Monolith Costs

  • Infrastructure: Single instance, simple to scale
  • Development: Faster initial development
  • Operations: Simpler deployment and monitoring

Microservices Costs

  • Infrastructure: Multiple services, load balancers, service mesh
  • Development: More complex initially, faster for large teams
  • Operations: Distributed tracing, service discovery, more complex deployments

Real-world example:

  • Monolith: 1 EC2 instance ($100/month)
  • Microservices: 5 services + load balancer + monitoring ($500/month)

Decision Framework

Ask yourself:

  1. Team Size: < 10 engineers? → Monolith
  2. Iteration Speed: Need to pivot quickly? → Monolith
  3. Scaling Needs: Uniform scaling OK? → Monolith
  4. Operational Capability: Limited DevOps experience? → Monolith
  5. Independent Deployment: Teams blocked by each other? → Microservices
  6. Different Scaling: Some parts need 10x more resources? → Microservices

Hybrid Approach

Many successful companies use:

  1. Monolith for core business logic
  2. Microservices for:
    • Resource-intensive tasks (ML inference, image processing)
    • Third-party integrations
    • Experimental features
    • Different scaling requirements

Conclusion

Don't choose microservices because they're trendy. Choose them when:

  • You have real scaling or organizational needs
  • You have the operational maturity
  • The benefits outweigh the complexity cost

Start with a well-structured monolith. If you outgrow it, you'll know—your deployment will slow down, teams will block each other, and scaling will become challenging.

Remember: The goal is to deliver value to users, not to have the coolest architecture. Choose the simplest thing that meets your needs.

Tags:
Architecture
Microservices
Monoliths
System Design
Best Practices

Want to read more articles?