What are Microservices?
Microservices is an architectural style where an application is built as a collection of small, independent services. Each service runs in its own process, owns its data, and communicates with other services through well-defined APIs.
Think of it like a restaurant kitchen. Instead of one chef doing everything, you have specialized stations: one for grilling, one for salads, one for desserts. Each station (service) does one thing well and communicates with others to deliver the complete meal.
Monolith vs Microservices
Monolith: Microservices:
┌─────────────────────────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ │ │ User │ │ Order │ │ Pay │
│ Single Application │ │Service│ │Service│ │Service│
│ │ └───┬───┘ └───┬───┘ └───┬───┘
│ Users Orders Payment │ │ │ │
│ │ ┌───┴───┐ ┌───┴───┐ ┌───┴───┐
│ Single Database │ │ DB │ │ DB │ │ DB │
└─────────────────────────┘ └───────┘ └───────┘ └───────┘
Monolith: Microservices:
+ Simple to develop + Independent deployment
+ Easy to test + Technology flexibility
+ Simple deployment + Scalability per service
- Hard to scale parts + Fault isolation
- One failure affects all - Complex infrastructure
- Technology lock-in - Network complexity
- Data consistency challenges
Core Principles
- Single Responsibility: Each service does one thing well
- Autonomy: Services can be developed, deployed, and scaled independently
- Decentralization: No central database or governance
- Failure Isolation: One service failing doesn't crash others
- Business Domain: Services organized around business capabilities
- Smart Endpoints: Services contain logic, not the communication layer
Communication Patterns
1. Synchronous (HTTP/REST)
# Order Service calls User Service
import httpx
async def get_user_for_order(user_id: int):
async with httpx.AsyncClient() as client:
response = await client.get(f"http://user-service/users/{user_id}")
return response.json()
# Simple, but creates coupling
# If User Service is down, Order Service fails
2. Asynchronous (Message Queue)
# Order Service publishes event
import pika
def publish_order_created(order_data):
connection = pika.BlockingConnection(
pika.ConnectionParameters('rabbitmq')
)
channel = connection.channel()
channel.queue_declare(queue='orders')
channel.basic_publish(
exchange='',
routing_key='orders',
body=json.dumps(order_data)
)
# Payment Service consumes event
def callback(ch, method, properties, body):
order = json.loads(body)
process_payment(order)
channel.basic_consume(queue='orders', on_message_callback=callback)
Python Microservices Example
Let's build a simple e-commerce system with three services:
Project Structure
ecommerce/
├── user-service/
│ ├── app/
│ │ ├── main.py
│ │ └── models.py
│ ├── requirements.txt
│ └── Dockerfile
├── order-service/
│ ├── app/
│ │ ├── main.py
│ │ └── models.py
│ ├── requirements.txt
│ └── Dockerfile
├── notification-service/
│ ├── app/
│ │ └── main.py
│ ├── requirements.txt
│ └── Dockerfile
└── docker-compose.yml
User Service
# user-service/app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="User Service")
# In-memory database (use PostgreSQL in production)
users_db = {}
class User(BaseModel):
id: int
name: str
email: str
@app.post("/users/")
def create_user(user: User):
users_db[user.id] = user
return user
@app.get("/users/{user_id}")
def get_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
return users_db[user_id]
@app.get("/health")
def health():
return {"status": "healthy"}
Order Service
# order-service/app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx
import os
app = FastAPI(title="Order Service")
USER_SERVICE_URL = os.getenv("USER_SERVICE_URL", "http://user-service:8000")
orders_db = {}
class Order(BaseModel):
id: int
user_id: int
product: str
amount: float
@app.post("/orders/")
async def create_order(order: Order):
# Verify user exists
async with httpx.AsyncClient() as client:
response = await client.get(f"{USER_SERVICE_URL}/users/{order.user_id}")
if response.status_code != 200:
raise HTTPException(status_code=400, detail="User not found")
orders_db[order.id] = order
# Publish event for notification service
# In production, use RabbitMQ or Kafka
return {"order": order, "status": "created"}
@app.get("/orders/{order_id}")
def get_order(order_id: int):
if order_id not in orders_db:
raise HTTPException(status_code=404, detail="Order not found")
return orders_db[order_id]
Docker Compose
# docker-compose.yml
version: '3.8'
services:
user-service:
build: ./user-service
ports:
- "8001:8000"
environment:
- DATABASE_URL=postgresql://user:pass@user-db/users
order-service:
build: ./order-service
ports:
- "8002:8000"
environment:
- USER_SERVICE_URL=http://user-service:8000
- DATABASE_URL=postgresql://user:pass@order-db/orders
depends_on:
- user-service
notification-service:
build: ./notification-service
environment:
- RABBITMQ_URL=amqp://rabbitmq
depends_on:
- rabbitmq
rabbitmq:
image: rabbitmq:3-management
ports:
- "15672:15672"
api-gateway:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- user-service
- order-service
API Gateway Pattern
# nginx.conf - Simple API Gateway
events {}
http {
upstream user_service {
server user-service:8000;
}
upstream order_service {
server order-service:8000;
}
server {
listen 80;
location /users {
proxy_pass http://user_service;
}
location /orders {
proxy_pass http://order_service;
}
}
}
# Clients only know about one endpoint (the gateway)
# Gateway routes to appropriate services
Common Patterns
Service Discovery
Services need to find each other. Options: DNS, Consul, Kubernetes DNS.
Circuit Breaker
Prevent cascade failures when a service is down.
# Using circuitbreaker library
from circuitbreaker import circuit
@circuit(failure_threshold=5, recovery_timeout=30)
async def call_user_service(user_id):
response = await client.get(f"{USER_SERVICE}/users/{user_id}")
return response.json()
# After 5 failures, circuit opens and calls fail fast
# After 30 seconds, tries again
Event Sourcing
Store events instead of current state. Rebuild state from events.
Saga Pattern
Manage distributed transactions across services.
When to Use Microservices
Good for:
- Large teams that can own individual services
- Applications with varying scaling needs
- Systems requiring technology diversity
- Organizations with DevOps maturity
Not ideal for:
- Small teams or simple applications
- Early-stage startups finding product-market fit
- Teams without containerization experience
Start with a monolith, extract microservices when needed.
Master Microservices with Expert Mentorship
Our Full Stack Python program covers microservices architecture with FastAPI. Learn to build, deploy, and manage distributed systems with personalized guidance.
Explore Full Stack Python Program