Microservices¶
Microservices are the architecture where everything is someone else's problem until it's your problem at 3AM because service A can't reach service B and service C is silently dropping messages and nobody can figure out why because each team only owns their piece The microservices cheat code: don't start with them. Start with a well-structured monolith , find the pain points , split those out. Premature microservices are an anti-pattern that multiplies complexity without delivering value
Monolith vs Microservices — The Honest Comparison¶
Monolith:
flowchart TB
subgraph Monolith_App[Monolith Application]
Auth[Auth Module]
Users[Users Module]
Payments[Payments Module]
Blog[Blog Module]
Notify[Notify Module]
Search[Search Module]
end
DB[(Single Database)]
Monolith_App --> DB Microservices:
flowchart TB
Auth[Auth Service<br/>DB: auth]
Users[Users Service<br/>DB: users]
Payments[Payments Service<br/>DB: pay]
MQ[Message Queue<br/>RabbitMQ / Kafka]
Blog[Blog Service<br/>DB: blog]
Notify[Notify Service<br/>DB: notify]
Search[Search Service<br/>DB: es]
Auth --> MQ
Users --> MQ
Payments --> MQ
MQ --> Blog
MQ --> Notify
MQ --> Search When monolith wins: * Team of 1-5 developers * Single domain with clear boundaries * No independent scaling needs * You want to ship fast without 10 services to start
When microservices win: * Team of 20+ across multiple squads * Different services need different scaling (payments needs more than auth) * Polyglot persistence (different DB types per service) * Independent deploy cycles per team
When to Break Things Apart¶
You don't decompose a monolith — you extract services from it Signals it's time:
- Deploy coupling — a change to any feature requires full redeploy , risking the entire app
- Team bottlenecks — 15 people PRing into the same codebase , merge conflicts every day
- Database contention — one table locks everything , the reporting query runs for 30 seconds and blocks writes
- Different scaling needs — search needs 10 instances , auth needs 2 , but they're all in the same deploy
- Tech stack divergence — you want to use a different language or framework for one feature
The extraction pattern:
Phase 1: Identify the bounded context → auth is separate from blog
Phase 2: Extract data ownership → auth owns user table , blog reads via API
Phase 3: Extract service layer → standalone auth service with its own DB
Phase 4: Split deployment → auth deploys independently
Inter-Service Communication — How Services Talk¶
REST (simple , request-response):
// Service A calls Service B via HTTP
const response = await fetch('http://user-service:3001/api/users/123')
const user = await response.json()
- Pros: Simple , everyone knows HTTP , easy to debug
- Cons: Synchronous , latency adds up (A calls B calls C calls D) , cascading failures
gRPC (fast , typed , binary):
// user.proto
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}
message GetUserRequest {
string user_id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
// Node.js gRPC client
const client = new UserService('user-service:50051', grpc.credentials.createInsecure())
client.getUser({ userId: '123' }, (err, user) => {
console.log(user.name)
})
- Pros: Fast (binary protocol) , strongly typed (protobuf) , streaming support
- Cons: More complex setup , tooling for debugging is limited , versioning is hard
Message Queues (async , decoupled):
// Service A publishes an event
await channel.publish('exchange', 'user.created', Buffer.from(JSON.stringify({
userId: '123',
email: 'user@example.com'
})))
// Service B consumes the event
channel.consume('queue.notifications', (msg) => {
const event = JSON.parse(msg.content.toString())
sendWelcomeEmail(event.email)
channel.ack(msg)
})
- Pros: Decoupled , resilient (queue buffers if consumer is down) , supports fan-out patterns
- Cons: Eventual consistency , harder to debug , message ordering challenges
Service Discovery — How Services Find Each Other¶
In a monolith , function calls just work. In microservices , Service A needs to find Service B's IP address — and with auto-scaling , IPs change constantly
DNS-based discovery (simple):
# Kubernetes handles this — service names resolve to active pods
user-service: The name "user-service" resolves to one of the healthy pods
Consul/HashiCorp (dedicated service registry):
# Register service
consul services register -name=user-service -address=10.0.1.5 -port=3001
# Discover service
dig user-service.service.consul
# Returns 10.0.1.5 — or any healthy instance
Distributed Tracing — Following Requests Across Services¶
Without tracing , debugging a "request failed" error across 5 services is like finding a specific cell in a spreadsheet by printing it out and using a magnifying glass
// Every service propagates trace context in headers
const response = await fetch('http://user-service:3001/api/users', {
headers: {
'x-request-id': req.id,
'x-trace-id': currentTrace.id,
'x-span-id': currentSpan.id
}
})
Jaeger or Zipkin visualize the trace:
flowchart LR
A[Service A - 100ms] --> B[Service B - 200ms]
B --> C[Service C - 500ms]
B --> D[Service D - 300ms] Eventual Consistency — Data Across Services¶
Each service owns its data. There's no JOIN across services. If you need data from multiple services , you either:
API composition (request-time joining):
// Backend-for-frontend (BFF) aggregates
async function getUserProfile(userId) {
const [user, orders, notifications] = await Promise.all([
userService.getUser(userId),
orderService.getOrders(userId),
notifyService.getNotifications(userId)
])
return { user, orders, notifications }
}
Eventual consistency (data replication via events):
// User service publishes event
await messageQueue.publish('user.updated', { userId: '123', name: 'New Name' })
// Blog service subscribes and updates its local copy
messageQueue.subscribe('user.updated', (event) => {
await blogDb.query('UPDATE authors SET name = $1 WHERE user_id = $2',
[event.name, event.userId])
})
The Saga Pattern — Distributed Transactions¶
A saga is a sequence of local transactions where each step publishes an event that triggers the next. If a step fails , compensating transactions undo previous steps
Order processing saga:
sequenceDiagram
participant Order as Order Service
participant Payment as Payment Service
participant Inventory as Inventory Service
participant Shipping as Shipping Service
Order->>Payment: Reserve inventory
Payment->>Inventory: Process payment
Inventory->>Shipping: Deduct stock
Shipping-->>Inventory: Schedule shipment - FAILED
Inventory-->>Payment: Restore stock (compensating)
Payment-->>Order: Refund (compensating)
Order-->>Order: Cancel order (compensating) // Choreography-based saga (services react to events)
// Order Service
async function createOrder(order) {
await db.query('INSERT INTO orders ...')
await messageQueue.publish('order.created', { orderId: order.id })
}
// Payment Service
messageQueue.subscribe('order.created', async (event) => {
try {
await processPayment(event.orderId)
await messageQueue.publish('payment.processed', event)
} catch (err) {
await messageQueue.publish('payment.failed', event)
}
})
// Inventory Service
messageQueue.subscribe('payment.processed', async (event) => {
try {
await deductInventory(event.orderId)
await messageQueue.publish('inventory.deducted', event)
} catch (err) {
// Refund payment — compensating action
await messageQueue.publish('payment.refund', event)
}
})
next → upcoming_04_serverless.md