Microservices promise independent deployments, team autonomy, and fault isolation. But they also bring distributed system complexity, network latency, and data consistency challenges. In this lesson, you'll learn when to use microservices, how to design them, and the patterns that make them work.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Determine when to use microservices vs monolith
  • Decompose a monolith into services
  • Choose service communication patterns (sync vs async)
  • Handle distributed data consistency (Saga pattern)
  • Implement API gateways and service discovery
  • Design for failure with circuit breakers

2. Why This Matters

Real-world scenario: Your monolith has 50 engineers committing to the same codebase. Deployments take hours. One team's bug takes down the entire system. Microservices offer a path to independent teams, independent deploys, and fault isolation.

3. Core Concepts

Monolith vs Microservices

Monolith:
┌─────────────────────────────────────────────────────────────────┐
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐           │
│  │  User   │  │ Product │  │  Order  │  │Payment  │           │
│  │ Module  │  │ Module  │  │ Module  │  │ Module  │           │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘           │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                     Shared Database                      │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Microservices:
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│  User   │  │ Product │  │  Order  │  │Payment  │
│ Service │  │ Service │  │ Service │  │ Service │
└────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘
     │            │            │            │
     └────────────┼────────────┼────────────┘
                  │            │
              ┌───┴───┐    ┌───┴───┐
              │User DB│    │Order DB│
              └───────┘    └───────┘

When to start with microservices:
✗ Startup with 3 engineers → NO
✗ Unknown domain → NO
✗ Simple CRUD app → NO
✓ Large team (50+ engineers) → YES
✓ Multiple independent teams → YES
✓ Different scaling requirements → YES
✓ Different technology preferences → YES
Monolith vs Microservices comparison

Decomposition Strategies

How to decompose a monolith:

1. Decompose by Business Capability
   ┌─────────────────────────────────────────────────────────────────┐
   │ Business capabilities: Order Management, Inventory, Shipping  │
   │ Each becomes a service                                        │
   └─────────────────────────────────────────────────────────────────┘

2. Decompose by Subdomain (DDD)
   ┌─────────────────────────────────────────────────────────────────┐
   │ Domain: E-commerce                                            │
   │ Subdomains: Catalog, Cart, Checkout, Payment, Fulfillment     │
   └─────────────────────────────────────────────────────────────────┘

3. Decompose by Verb (use case)
   ┌─────────────────────────────────────────────────────────────────┐
   │ Action: Search, Browse, Purchase, Return                      │
   │ Each action becomes a service? Not recommended                │
   │ Better: Nouns (resources) for REST, not verbs                 │
   └─────────────────────────────────────────────────────────────────┘

4. Strangler Fig Pattern (gradual migration)
   ┌─────────────────────────────────────────────────────────────────┐
   │ Phase 1: Identify a bounded context to extract                │
   │ Phase 2: Build new service alongside monolith                 │
   │ Phase 3: Route traffic to new service                         │
   │ Phase 4: Remove old code                                      │
   │ Phase 5: Repeat for next bounded context                      │
   └─────────────────────────────────────────────────────────────────┘
Monolith decomposition strategies

Service Communication Patterns

Communication Patterns:

1. Synchronous (REST, gRPC)
   ┌─────────────────────────────────────────────────────────────────┐
   │ User Service ──HTTP Request──► Order Service                  │
   │                ◄──Response──                                   │
   │                                                                 │
   │ Pros: Simple, easy to understand                              │
   │ Cons: Coupling, cascading failures, blocking                  │
   │ Use: When immediate response needed, low latency tolerance     │
   └─────────────────────────────────────────────────────────────────┘

2. Asynchronous (Message Queue)
   ┌─────────────────────────────────────────────────────────────────┐
   │ User Service ──Message──► Queue ──► Order Service             │
   │                                                                 │
   │ Pros: Decoupled, resilient, buffered                          │
   │ Cons: Complex eventual consistency, harder to debug           │
   │ Use: When immediate response not needed, high throughput       │
   └─────────────────────────────────────────────────────────────────┘

3. Event-Driven (Pub/Sub)
   ┌─────────────────────────────────────────────────────────────────┐
   │ User Service ──Event──► Event Bus ──► Order Service           │
   │                              ├──► Inventory Service            │
   │                              └──► Email Service                │
   │                                                                 │
   │ Pros: Very decoupled, many consumers                          │
   │ Cons: Eventual consistency, ordering challenges               │
   │ Use: When multiple services need to react to events            │
   └─────────────────────────────────────────────────────────────────┘
Service communication patterns

API Gateway Pattern

API Gateway Architecture:

                    ┌─────────────────────────────────────┐
                    │            Client                    │
                    └───────────────┬─────────────────────┘
                                    │
                                    ▼
                    ┌─────────────────────────────────────┐
                    │           API Gateway                │
                    │  • Authentication                    │
                    │  • Rate Limiting                     │
                    │  • Request Routing                   │
                    │  • Response Aggregation              │
                    │  • Caching                           │
                    │  • Logging/Monitoring                │
                    └───────────────┬─────────────────────┘
                                    │
            ┌───────────┬───────────┼───────────┬───────────┐
            │           │           │           │           │
            ▼           ▼           ▼           ▼           ▼
       ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐
       │ User   │  │ Product│  │ Order  │  │Payment │  │Inventory│
       │ Service│  │ Service│  │ Service│  │ Service│  │ Service│
       └────────┘  └────────┘  └────────┘  └────────┘  └────────┘

API Gateway Example (Kong/Kong):
# kong.yml
_format_version: "2.1"
services:
- name: user-service
  url: http://user-service:8080
  routes:
  - name: user-route
    paths:
    - /users
  plugins:
  - name: rate-limiting
    config:
      minute: 100
  - name: jwt
    config:
      secret_is_base64: false

- name: order-service
  url: http://order-service:8081
  routes:
  - name: order-route
    paths:
    - /orders
  plugins:
  - name: cors
    config:
      origins:
      - https://example.com
API Gateway pattern

Service Discovery

# Consul service discovery configuration
# Register service
curl -X PUT http://localhost:8500/v1/agent/service/register -d '
{
  "ID": "user-service-1",
  "Name": "user-service",
  "Address": "10.0.1.10",
  "Port": 8080,
  "Check": {
    "HTTP": "http://10.0.1.10:8080/health",
    "Interval": "10s"
  }
}'

# Service discovery from application
import consul

c = consul.Consul()

# Find healthy instances of a service
index, services = c.health.service('user-service', passing=True)
for service in services:
    address = service['Service']['Address']
    port = service['Service']['Port']
    print(f"Found user-service at {address}:{port}")

# Kubernetes service discovery (built-in)
# Services automatically get DNS names
# user-service.default.svc.cluster.local

# Use env vars for service discovery (simplest)
# In Kubernetes, set environment variables in deployment:
# env:
# - name: USER_SERVICE_HOST
#   value: user-service.default.svc.cluster.local
# - name: USER_SERVICE_PORT
#   value: "8080"
Service discovery patterns

Circuit Breaker Pattern

# Circuit breaker implementation
import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"      # Normal operation
    OPEN = "open"          # Failing, don't call
    HALF_OPEN = "half_open" # Testing if recovered

class CircuitBreaker:
    def __init__(self, failure_threshold=5, timeout=60):
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.failures = 0
        self.state = CircuitState.CLOSED
        self.last_failure_time = None

    def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.timeout:
                self.state = CircuitState.HALF_OPEN
                print("Circuit half-open, testing...")
            else:
                raise Exception("Circuit breaker is OPEN")

        try:
            result = func(*args, **kwargs)
            if self.state == CircuitState.HALF_OPEN:
                print("Circuit closed (recovered)")
                self.reset()
            return result
        except Exception as e:
            self.failures += 1
            self.last_failure_time = time.time()

            if self.failures >= self.failure_threshold:
                self.state = CircuitState.OPEN
                print(f"Circuit OPEN after {self.failures} failures")
            raise e

    def reset(self):
        self.failures = 0
        self.state = CircuitState.CLOSED

# Usage
user_service_cb = CircuitBreaker(failure_threshold=3, timeout=30)

def get_user(user_id):
    # Call user service
    response = requests.get(f"http://user-service/users/{user_id}")
    response.raise_for_status()
    return response.json()

try:
    user = user_service_cb.call(get_user, 123)
except Exception:
    # Fallback: return cached user data
    user = get_cached_user(123)

# In production, use libraries:
# - Resilience4j (Java)
# - Polly (.NET)
# - Tenacity (Python)
# - Hystrix (legacy)
Circuit breaker pattern implementation

4. Complete Project: Microservices Reference Architecture

# docker-compose-microservices.yml
version: '3.8'

services:
  # API Gateway
  kong:
    image: kong:latest
    environment:
      KONG_DATABASE: off
      KONG_DECLARATIVE_CONFIG: /etc/kong/kong.yml
      KONG_PROXY_ACCESS_LOG: /dev/stdout
      KONG_ADMIN_ACCESS_LOG: /dev/stdout
    volumes:
      - ./kong.yml:/etc/kong/kong.yml
    ports:
      - "8000:8000"
      - "8001:8001"
    depends_on:
      - user-service
      - order-service
      - product-service

  # Service Discovery (Consul)
  consul:
    image: consul:latest
    ports:
      - "8500:8500"
    command: agent -dev -client=0.0.0.0

  # User Service
  user-service:
    build: ./services/user
    environment:
      CONSUL_HOST: consul
      DB_HOST: user-db
    ports:
      - "8081:8080"
    depends_on:
      - consul
      - user-db
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s

  user-db:
    image: postgres:15
    environment:
      POSTGRES_DB: users
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - user-db-data:/var/lib/postgresql/data

  # Product Service
  product-service:
    build: ./services/product
    environment:
      CONSUL_HOST: consul
      DB_HOST: product-db
    ports:
      - "8082:8080"
    depends_on:
      - consul
      - product-db

  product-db:
    image: mongodb:7
    volumes:
      - product-db-data:/data/db

  # Order Service
  order-service:
    build: ./services/order
    environment:
      CONSUL_HOST: consul
      DB_HOST: order-db
      PRODUCT_SERVICE_URL: http://product-service:8080
    ports:
      - "8083:8080"
    depends_on:
      - consul
      - order-db
      - product-service
      - user-service

  order-db:
    image: postgres:15
    environment:
      POSTGRES_DB: orders
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - order-db-data:/var/lib/postgresql/data

  # Message Queue (for async events)
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"

  # Notification Service (async consumer)
  notification-service:
    build: ./services/notification
    environment:
      RABBITMQ_HOST: rabbitmq
    depends_on:
      - rabbitmq

  # Analytics Service (event consumer)
  analytics-service:
    build: ./services/analytics
    environment:
      RABBITMQ_HOST: rabbitmq
    depends_on:
      - rabbitmq

volumes:
  user-db-data:
  product-db-data:
  order-db-data:

networks:
  default:
    name: microservices-network
Complete microservices reference architecture

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Congratulations! You've completed the Backend Architecture module!

You've learned:

  • Database design and scaling strategies
  • Caching with Redis
  • Message queues (RabbitMQ, Kafka)
  • API design (REST vs GraphQL)
  • Microservices architecture patterns

Continue to cloud computing modules or practice building a complete system using these patterns.

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen March 7, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment