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
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 │
└─────────────────────────────────────────────────────────────────┘
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 │
└─────────────────────────────────────────────────────────────────┘
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
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"
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)
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
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.
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment