Chapter 31: Microservices Architecture
Core Principle: Decompose systems into independently deployable services organized around business capabilities, enabling autonomous teams and technology flexibility.
Key Tradeoffs: Operational complexity and distributed system challenges vs. deployment independence and team autonomy.
1. Monolith vs Microservices
| Dimension | Monolith | Microservices | Impact |
|---|---|---|---|
| Deployment | Single deployable unit | 100s of independent services | Monolith: all-or-nothing, coordinated release. Micro: deploy anytime, rollback individually |
| Scaling | Scale entire application | Scale individual services | Monolith: over-provision resources. Micro: optimize per-service (payment 20x, admin 2x) |
| Team Ownership | Shared codebase, coordination overhead | Team owns full service lifecycle | Micro enables Conway's Law alignment: team structure = architecture |
| Fault Isolation | Bug crashes entire app | Service failure isolated (with circuit breakers) | Micro: recommendation service down, checkout still works |
| Technology Choice | Single stack (Java, .NET) | Polyglot (Go for perf, Python for ML) | Micro: right tool per job. Risk: fragmentation, support burden |
| Complexity | Code complexity in one place | Operational complexity (networking, observability, deployment) | Micro requires mature DevOps, distributed tracing, service mesh |
| Data Consistency | ACID transactions | Eventual consistency, sagas | Micro: no distributed transactions, compensating actions required |
| Latency | In-process calls (nanoseconds) | Network calls (milliseconds) | Micro: 5-hop request = 100ms overhead. Sync chains amplify latency |
| Testing | Full integration tests easy | Contract testing, test environments hard | Micro: coordinating 50 services in staging is painful |
| Onboarding | Understand one codebase | Understand distributed system | Monolith: simpler for juniors. Micro: higher cognitive load |
When a Monolith is RIGHT
| Scenario | Why Monolith Wins |
|---|---|
| Startup / MVP | Domain not understood yet. Premature decomposition locks in wrong boundaries. Start monolith, extract services when pain emerges. |
| Small Team (<10) | Microservices overhead (deployment, monitoring, tracing) exceeds coordination cost. Conway's Law: small team = natural monolith. |
| High Transaction Cohesion | Checkout = validate inventory + charge payment + create order. If these always change together, one service with ACID is simpler than 3 services with sagas. |
| Immature DevOps | No CI/CD, no containerization, no observability = microservices disaster. Build monolith, invest in platform, then migrate. |
| Uniform Scaling | If all components scale together (internal admin tool), microservices add no scaling benefit. |
Interview Question: When would you NOT use microservices?
Strong Answer: "I'd avoid microservices for an MVP where the domain is unclear. Premature decomposition locks in wrong boundaries that are hard to fix. I'd start with a modular monolith using clear module boundaries, then extract services once we identify high-change areas, scaling bottlenecks, or team ownership boundaries. Also avoid if the team lacks mature CI/CD—microservices multiply operational complexity."
2. Decomposition Strategies
Strategy 1: By Business Capability
| Capability | What It Does | Service |
|---|---|---|
| Product Catalog | Manage product info, search | product-service |
| Order Management | Create orders, track status | order-service |
| Inventory | Track stock levels, reserve items | inventory-service |
| Payment Processing | Charge cards, handle refunds | payment-service |
| Shipping | Calculate rates, track shipments | shipping-service |
Advantage: Stable boundaries (business capabilities change slowly). Teams align with business stakeholders.
Strategy 2: By Subdomain (Domain-Driven Design)
| Subdomain Type | Definition | Service Strategy | Example |
|---|---|---|---|
| Core Domain | Competitive advantage, high complexity | Fine-grained services, most investment | Pricing engine, recommendation ML |
| Supporting Domain | Necessary but not differentiating | Medium granularity, buy if available | Order management, inventory |
| Generic Domain | Commodity, no customization needed | Buy or coarse-grained service | Email sending, payment gateway |
Strategy 3: By Verb/Noun
- Noun (Entity Services): Customer Service, Product Service → data ownership, CRUD operations
- Verb (Process Services): Checkout Service, Fraud Detection → orchestrate workflows across entities
Risk: Verb services become orchestration layers with no logic (anemic). Prefer business capability.
Strategy 4: Strangler Fig Pattern (for migration)
graph LR
A[Client] --> B[API Gateway]
B -->|New Traffic| C[Microservice 1]
B -->|New Traffic| D[Microservice 2]
B -->|Old Traffic| E[Monolith]
style C fill:#90EE90
style D fill:#90EE90
style E fill:#FFB6C1
- Identify bounded context to extract (e.g., catalog)
- Build new microservice with same API
- Route traffic via gateway: 10% → microservice, 90% → monolith
- Gradually increase percentage, monitor, rollback if issues
- When 100% on microservice, remove monolith code
Decomposition Decision Diagram
graph TD
A[Want to extract service?] --> B{High change frequency or scaling need?}
B -->|No| C[Keep in monolith]
B -->|Yes| D{Clear business capability boundary?}
D -->|No| E[Refactor monolith first - extract bounded context]
D -->|Yes| F{Team can own end-to-end?}
F -->|No| G[Too fine-grained - combine with related capability]
F -->|Yes| H{Data dependencies manageable?}
H -->|No| I[Use shared DB temporarily or event sourcing]
H -->|Yes| J[Extract as microservice]
Interview Question: How would you decompose an e-commerce monolith?
Strong Answer: "I'd use business capability decomposition: catalog, inventory, order, payment, shipping. Start with the strangler fig pattern on the highest-pain area—say inventory has scaling issues during flash sales. Extract inventory-service with its own DB, route traffic via API gateway with canary rollout. Use event-driven communication: order-service publishes OrderCreated, inventory-service subscribes and reserves stock asynchronously. Avoid extracting too granularly—don't split 'add to cart' into a separate service; keep it with order management."
3. Bounded Contexts (DDD)
Bounded Context: An explicit boundary within which a domain model is consistent. Outside the boundary, different models apply.
| Concept | Definition | Example |
|---|---|---|
| Context Boundary | Semantic boundary where terms have specific meaning | "Product" in Catalog = name, price, description. "Product" in Inventory = SKU, quantity, warehouse. Different models! |
| Ubiquitous Language | Shared vocabulary within one context | Order context: "place order", "fulfill", "cancel". Consistent in code, docs, conversations. |
| Context Mapping | Define relationships between contexts | Upstream/Downstream, Customer/Supplier, Shared Kernel, Anti-Corruption Layer |
Context Map Patterns
| Pattern | Relationship | When to Use |
|---|---|---|
| Shared Kernel | Two contexts share subset of domain model | Tightly aligned teams, shared library. Risk: coupling. Minimize shared code. |
| Customer/Supplier | Downstream depends on upstream's API | Supplier defines API, customer consumes. Use versioning to avoid breaking downstream. |
| Conformist | Downstream accepts upstream's model as-is | Upstream is external (Stripe API). No negotiation, just adapt. |
| Anti-Corruption Layer (ACL) | Translation layer protects downstream from upstream changes | Legacy system, external API. Translate external model to internal model. |
| Open Host Service | Upstream publishes well-documented API for many consumers | Public API, internal platform service. Use REST/GraphQL with versioning. |
| Published Language | Standardized schema for integration | Event schema (Avro, Protobuf). All services speak same language for events. |
Bounded Context Example
graph TD
A[Catalog Context] -->|Customer/Supplier| B[Order Context]
B -->|ACL| C[Legacy ERP]
A -->|Published Language: ProductAvro| D[Event Bus]
B -->|Published Language: OrderAvro| D
E[Inventory Context] -->|Subscribe| D
F[Shipping Context] -->|Subscribe| D
G[Payment Context] -->|Open Host Service| B
style A fill:#ADD8E6
style B fill:#FFD700
style E fill:#90EE90
style F fill:#FFA07A
style G fill:#DDA0DD
Anti-Corruption Layer Pattern
// Order service internal model
class Order {
orderId: string
customerId: string
items: LineItem[]
totalAmount: Money
}
// Legacy ERP external model (awful)
class ERPOrder {
ORD_ID: number
CUST_NO: string
ITEMS: string // comma-separated SKUs
TOTAL: number // cents, USD only
}
// ACL translates between models
class ERPAdapter {
toInternal(erpOrder: ERPOrder): Order {
return {
orderId: `ORD-${erpOrder.ORD_ID}`,
customerId: erpOrder.CUST_NO,
items: erpOrder.ITEMS.split(',').map(parseSKU),
totalAmount: Money.cents(erpOrder.TOTAL, 'USD')
}
}
toExternal(order: Order): ERPOrder {
// reverse translation
}
}
Interview Question: What is a bounded context?
Strong Answer: "A bounded context is a boundary within which a domain model is internally consistent. For example, in e-commerce, 'Customer' in the Order context includes shipping address and payment method, but 'Customer' in the Support context includes ticket history and SLA tier. These are different models for the same entity. Each microservice should align with one bounded context to avoid model conflicts. If services must integrate, use an anti-corruption layer to translate between models rather than sharing a monolithic 'Customer' class."
4. Communication Patterns
| Pattern | Mechanism | Coupling | Use Case | Failure Mode |
|---|---|---|---|---|
| Synchronous Request/Response | REST, gRPC | Temporal coupling: caller waits | Query data (get product details), immediate consistency needed | Cascading failures if downstream is slow/down. Use timeouts, circuit breakers. |
| Asynchronous Messaging | RabbitMQ, SQS, Kafka | Decoupled: sender doesn't wait | Commands (process order), fire-and-forget | Message loss if broker fails. Ensure at-least-once delivery, idempotency. |
| Event-Driven | Kafka, EventBridge, pub/sub | Decoupled: publisher doesn't know subscribers | Broadcast state changes (OrderPlaced), multiple consumers | Eventual consistency, out-of-order events. Use event versioning, consumer idempotency. |
| GraphQL Federation | Apollo Federation | Client couples to unified schema | Aggregation layer for mobile/web, avoid over-fetching | Schema evolution pain if services change types. Use federation versioning. |
Sync vs Async Decision Table
| Scenario | Choice | Rationale |
|---|---|---|
| User clicks "Get Product Details" | Sync REST | User waits for response, need immediate result |
| User places order | Async event | Publish OrderPlaced, inventory/payment/shipping process independently. User doesn't wait for fulfillment. |
| Validate payment before order creation | Sync gRPC | Order service calls payment service, waits for validation before committing order |
| Send order confirmation email | Async message | Fire-and-forget, email sending is slow, don't block order API |
| Update search index when product changes | Async event | Catalog publishes ProductUpdated, search service subscribes, eventual consistency OK |
| Mobile app fetches order + product + shipping in one call | GraphQL BFF | Backend-for-Frontend aggregates 3 services, avoids mobile making 3 REST calls |
Communication Anti-Pattern: Sync Chain
sequenceDiagram
participant Client
participant API Gateway
participant Order Service
participant Inventory Service
participant Payment Service
participant Shipping Service
Client->>API Gateway: POST /orders
API Gateway->>Order Service: createOrder()
Order Service->>Inventory Service: reserveStock() [200ms]
Inventory Service-->>Order Service: OK
Order Service->>Payment Service: charge() [300ms]
Payment Service-->>Order Service: OK
Order Service->>Shipping Service: calculateRate() [150ms]
Shipping Service-->>Order Service: OK
Order Service-->>API Gateway: Order Created
API Gateway-->>Client: 201 [650ms total]
Problem: 650ms latency, one service failure fails entire request. Fix: Async choreography or saga orchestration.
Interview Question: When would you use async messaging vs sync REST?
Strong Answer: "Use sync REST when the caller needs an immediate response and can't proceed without it—like fetching product details for display. Use async messaging for fire-and-forget operations like sending emails, or when you want temporal decoupling so the caller doesn't wait—like publishing an OrderPlaced event that triggers inventory reservation, payment, and shipping independently. Async also prevents cascading failures: if inventory is down, the event stays in the queue and processes when it's back up. I'd avoid sync chains where one service calls 5 others sequentially—that amplifies latency and failure risk."
5. API Design
REST Best Practices
| Principle | Good | Bad |
|---|---|---|
| Resource-Oriented URLs | GET /orders/123 POST /orders DELETE /orders/123 |
GET /getOrder?id=123 POST /createOrder GET /deleteOrder/123 |
| HTTP Verbs | GET (safe, idempotent) POST (create) PUT (replace) PATCH (update) DELETE (idempotent) |
POST for everything GET with side effects |
| Status Codes | 200 OK, 201 Created, 204 No Content 400 Bad Request, 404 Not Found 500 Internal Error, 503 Unavailable |
200 for all responses with {error: true} in body |
| Pagination | GET /orders?page=2&size=20 Response: {items: [...], nextPage: 3} |
Return all 10,000 orders in one response |
| Filtering | GET /orders?status=shipped&since=2025-01-01 | POST /searchOrders with complex JSON filter |
| Versioning | Header: Accept: application/vnd.api+json;version=2 URL: /v2/orders (simpler) |
Breaking changes with no version |
gRPC for Internal Communication
| Feature | Benefit | Use Case |
|---|---|---|
| Protobuf | Compact binary, 3-10x smaller than JSON | High-throughput service-to-service (1M+ RPS) |
| Bidirectional Streaming | Client and server stream data concurrently | Real-time chat, stock ticker, IoT telemetry |
| Code Generation | Client stubs auto-generated from .proto | Type-safe, no manual serialization |
| HTTP/2 | Multiplexing, header compression | Reduce connection overhead vs REST |
When NOT gRPC: Browser clients (need gRPC-Web proxy), public APIs (REST more accessible), debugging (binary is hard to inspect).
GraphQL for Aggregation
// Client requests exactly what it needs
query {
order(id: "123") {
id
status
customer { name, email }
items { product { name, price }, quantity }
shipping { carrier, trackingNumber }
}
}
Benefit: One request aggregates order + customer + product + shipping (no over-fetching, no under-fetching).
Architecture: GraphQL gateway queries order-service, customer-service, product-service, shipping-service via REST/gRPC, stitches response.
Risk: N+1 queries (fetching 100 orders → 100 calls to customer-service). Fix: DataLoader batching.
API Versioning Strategy
| Change Type | Versioning Needed? | Example |
|---|---|---|
| Add optional field | No (backward compatible) | Add "loyaltyPoints" to Customer response |
| Add new endpoint | No | POST /orders/123/refund |
| Rename field | Yes (breaking) | Rename "customerName" → "fullName" |
| Remove field | Yes (breaking) | Remove "phoneNumber" from response |
| Change validation | Depends | Require email format (breaking if previously accepted garbage) |
Deployment: Run v1 and v2 side-by-side. Gateway routes by header/URL. Sunset v1 after 6 months.
6. Data Management
Database Per Service Pattern
| Principle | Rationale | Implementation |
|---|---|---|
| Exclusive DB Access | No shared DB. Each service owns its data schema. | Order service has order-db, Inventory has inventory-db. No cross-DB joins. |
| Polyglot Persistence | Choose DB per service needs | Product catalog: Postgres (relational). Search: Elasticsearch. Session: Redis. Cart: DynamoDB. |
| Data Duplication | Denormalize across services | Order service stores customerId + customerName (cached). Customer service is source of truth. Sync via events. |
Shared Database Anti-Pattern
graph TD
A[Order Service] --> B[(Shared DB)]
C[Inventory Service] --> B
D[Payment Service] --> B
E[Shipping Service] --> B
style B fill:#FF6B6B
Problem:
- Schema changes require coordinating 4 services
- No deployment independence (breaking schema change breaks all)
- DB becomes scaling bottleneck
- Tight coupling defeats microservices benefits
Exception: Read-only replica for analytics/reporting (not operational writes).
Data Duplication Patterns
| Pattern | How It Works | Consistency |
|---|---|---|
| Event Sourcing | Publish state changes as events. Consumers rebuild local views. | Eventual. CustomerUpdated event propagates to Order service, updates denormalized customerName. |
| CQRS (Command Query Separation) | Write model in one service, read model materialized in another | Eventual. Order service writes, Order-Query-Service materializes read-optimized view from events. |
| Change Data Capture (CDC) | Capture DB changes (Debezium reads Postgres WAL), publish to Kafka | Near real-time. Downstream services subscribe to DB change stream. |
| Synchronous Cache | Service queries upstream service, caches response | Cache staleness (TTL). Risk: temporal coupling if upstream is down. |
Handling Joins Across Services
// BAD: JOIN across services (impossible without shared DB)
SELECT o.id, c.name, p.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products p ON o.product_id = p.id
// GOOD: API Composition (sync)
orderService.getOrder(123) → { customerId: 456, productId: 789 }
customerService.getCustomer(456) → { name: "Alice" }
productService.getProduct(789) → { name: "Widget" }
// Stitch in API Gateway or BFF
// BETTER: Denormalize (async)
// Order service stores { customerId, customerName, productId, productName }
// When Customer changes name, publish CustomerUpdated event
// Order service subscribes, updates denormalized customerName
// Trade: stale data (eventual consistency) for query simplicity
Interview Question: How do you handle transactions across microservices?
Strong Answer: "You can't use distributed transactions (2PC is slow and brittle). Instead, use the Saga pattern with compensating transactions. For example, when placing an order, the workflow is: (1) Reserve inventory, (2) Charge payment, (3) Create order. If payment fails after inventory is reserved, publish a compensation event to release inventory. Implement as choreography (each service listens to events and publishes next step) or orchestration (central saga coordinator calls each service). I prefer orchestration for complex workflows because the coordinator has visibility into the entire state, making debugging easier."
7. Distributed Transactions
Saga Choreography
sequenceDiagram
participant Order
participant Inventory
participant Payment
participant Shipping
Order->>Order: Create order (pending)
Order->>EventBus: OrderCreated
EventBus->>Inventory: OrderCreated
Inventory->>Inventory: Reserve stock
Inventory->>EventBus: StockReserved
EventBus->>Payment: StockReserved
Payment->>Payment: Charge card
Payment->>EventBus: PaymentCompleted
EventBus->>Shipping: PaymentCompleted
Shipping->>Shipping: Schedule shipment
Shipping->>EventBus: ShipmentScheduled
EventBus->>Order: ShipmentScheduled
Order->>Order: Mark order confirmed
Compensation Flow (Payment Fails):
sequenceDiagram
participant Order
participant Inventory
participant Payment
Payment->>Payment: Charge card (FAIL)
Payment->>EventBus: PaymentFailed
EventBus->>Inventory: PaymentFailed
Inventory->>Inventory: Release stock
Inventory->>EventBus: StockReleased
EventBus->>Order: PaymentFailed
Order->>Order: Mark order cancelled
Saga Orchestration
graph TD
A[Order Saga Orchestrator] -->|1. Reserve stock| B[Inventory Service]
B -->|Success| A
A -->|2. Charge payment| C[Payment Service]
C -->|Success| A
A -->|3. Create shipment| D[Shipping Service]
D -->|Success| A
A -->|4. Confirm order| E[Order Service]
C -->|Failure| A
A -->|Compensate: Release stock| B
A -->|Compensate: Cancel order| E
Choreography vs Orchestration
| Dimension | Choreography | Orchestration |
|---|---|---|
| Coordination | Decentralized: each service reacts to events | Centralized: saga coordinator calls services |
| Coupling | Low: services don't know about each other | Higher: coordinator knows all services |
| Visibility | Hard: trace event flows across services | Easy: coordinator has full state machine |
| Complexity | Simple workflows. Complex workflows get messy (10 services, 20 events). | Handles complex workflows (conditional steps, retries, timeouts) |
| Tooling | Event bus (Kafka), distributed tracing | Temporal, AWS Step Functions, Camunda |
Idempotency for At-Least-Once Delivery
// Problem: Kafka delivers StockReserved event twice
// Without idempotency, inventory decrements twice
// Solution: Idempotency key (event ID)
class InventoryService {
private processedEvents = new Set()
onStockReserved(event: StockReserved) {
if (processedEvents.has(event.eventId)) {
return // already processed, skip
}
// Decrement stock in DB transaction
db.transaction(() => {
db.decrementStock(event.sku, event.quantity)
db.insertProcessedEvent(event.eventId) // atomic with business logic
})
processedEvents.add(event.eventId)
}
}
8. Service Mesh
What a Service Mesh Provides
| Feature | Without Service Mesh | With Service Mesh (Istio/Linkerd) |
|---|---|---|
| mTLS | Manually configure TLS in each service | Automatic cert rotation, encrypted service-to-service traffic |
| Observability | Instrument each service with metrics, logs, traces | Automatic distributed tracing, golden metrics (latency, errors, saturation) |
| Traffic Control | Code retries, timeouts, circuit breakers per service | Configure retries, timeouts, load balancing in mesh config (no code change) |
| Canary Deployment | Complex routing logic in API gateway | Route 10% traffic to v2, 90% to v1 via mesh rules |
| Rate Limiting | Each service implements rate limit logic | Global rate limits enforced by mesh |
Sidecar Proxy Model
graph LR
A[Service A] -->|localhost| B[Envoy Proxy A]
B -->|mTLS| C[Envoy Proxy B]
C -->|localhost| D[Service B]
E[Control Plane: Istiod] -.->|config| B
E -.->|config| C
How It Works:
- Every pod gets an Envoy sidecar proxy injected
- Service A sends request to localhost:80 (sidecar intercepts via iptables)
- Sidecar A establishes mTLS connection to Sidecar B, applies retries/timeouts/circuit breaker
- Sidecar B forwards to Service B on localhost
- Control plane (Istiod) pushes traffic rules, mTLS certs to sidecars
Service Mesh Configuration Example (Istio)
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- match:
- headers:
version:
exact: v2
route:
- destination:
host: order-service
subset: v2
weight: 10
- route:
- destination:
host: order-service
subset: v1
weight: 90
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: order-service
spec:
host: order-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 30s
Effect: 10% of traffic with header "version: v2" goes to v2 pods. Circuit breaker ejects pod after 5 consecutive 5xx errors.
When NOT to Use Service Mesh
- Small deployments (<10 services): Overhead (CPU, memory, complexity) exceeds benefit
- Non-Kubernetes: Service mesh tooling tightly coupled to K8s
- Performance-critical: Sidecar adds 1-5ms latency per hop
Interview Question: What is a service mesh and when would you use it?
Strong Answer: "A service mesh is an infrastructure layer that handles service-to-service communication via sidecar proxies. It provides mTLS for security, distributed tracing for observability, and traffic management like retries, timeouts, and canary deployments—all without changing application code. I'd use a service mesh like Istio when you have 50+ microservices and need centralized control over cross-cutting concerns. For a small deployment, the operational complexity isn't worth it—just use a library like Resilience4j for retries and circuit breakers."
9. Testing Strategies
Testing Pyramid for Microservices
graph TD
A[End-to-End Tests: 5%] --> B[Integration Tests: 20%]
B --> C[Contract Tests: 25%]
C --> D[Unit Tests: 50%]
style A fill:#FF6B6B
style B fill:#FFA07A
style C fill:#FFD700
style D fill:#90EE90
| Test Type | Scope | Speed | What It Validates | Example |
|---|---|---|---|---|
| Unit Tests | Single function/class | Milliseconds | Business logic, edge cases | OrderCalculator.calculateTotal() returns correct tax |
| Integration Tests | Service + DB/queue | Seconds | Service interacts correctly with dependencies | OrderService.createOrder() writes to DB, publishes event to Kafka |
| Contract Tests | API contract between services | Seconds | Consumer expectations match provider's API | Order service expects Customer API to return {id, name, email}. Test fails if Customer removes email. |
| Component Tests | Full service in isolation | Seconds | Service behavior with mocked dependencies | Deploy Order service, mock Inventory/Payment APIs, test full workflow |
| End-to-End Tests | All services in staging | Minutes | Happy path, critical user journeys | User places order → all services process → order confirmed. Flaky, slow, expensive to maintain. |
Contract Testing with Pact
// Consumer: Order Service expects Customer API contract
describe('Customer API', () => {
const provider = new Pact({ consumer: 'order-service', provider: 'customer-service' })
it('returns customer by ID', async () => {
await provider.addInteraction({
state: 'customer 123 exists',
uponReceiving: 'a request for customer 123',
withRequest: { method: 'GET', path: '/customers/123' },
willRespondWith: {
status: 200,
body: { id: '123', name: 'Alice', email: 'alice@example.com' }
}
})
const customer = await orderService.getCustomer('123')
expect(customer.email).toBe('alice@example.com')
})
})
// Provider: Customer Service verifies it satisfies contract
describe('Customer Service Pact', () => {
it('satisfies order-service contract', async () => {
await pact.verifyProvider({
provider: 'customer-service',
pactUrls: ['http://pact-broker/order-service/customer-service/latest']
})
})
})
Benefit: If Customer service removes "email" field, provider test fails, blocking deployment before breaking Order service.
Test Data Management
| Challenge | Solution |
|---|---|
| Shared test environment | Each service team gets isolated namespace in K8s. Use service virtualization (WireMock) to mock dependencies. |
| Test data coupling | Each test creates its own data (Builder pattern). Avoid shared fixtures that break when one test changes them. |
| Async event delays | Use polling with timeout: waitFor(() => orderStatus === 'confirmed', 5000ms). Avoid sleep(1000) (flaky). |
10. Anti-Patterns
| Anti-Pattern | Description | Consequence | Fix |
|---|---|---|---|
| Distributed Monolith | 100 services, but all must deploy together | Microservices complexity without independence benefits | Decouple via events, use API versioning to allow independent deployment |
| Nano-Services | One function per service (e.g., "CalculateTaxService") | Network overhead, deployment explosion, hard to understand | Merge related functions into coarser-grained service (OrderProcessingService) |
| Shared Libraries for Business Logic | 20 services depend on order-processing-lib v1.2.3 | Changing library requires redeploying all 20 services (defeats independence) | Shared libraries OK for cross-cutting (logging, metrics). Business logic must be in services or duplicated. |
| Synchronous Chains | API Gateway → Service A → B → C → D (4 sync hops) | Latency amplification, cascading failures | Use async events for fire-and-forget. Use BFF to aggregate data in one hop. |
| Shared Database | 10 services write to same Postgres DB | Schema changes break multiple services, DB becomes bottleneck | Database per service. Use events to sync denormalized data. |
| No API Gateway | Mobile app calls 50 services directly | Client couples to internal services, no authentication/rate limiting choke point | API Gateway (Kong, AWS API Gateway) or BFF for aggregation, auth, rate limiting |
| Microservices for Greenfield | Start new project with 30 microservices | Domain not understood, boundaries wrong, hard to refactor | Start with modular monolith. Extract services when scaling/team boundaries emerge. |
| Ignoring Fallacies of Distributed Computing | Assume network is reliable, latency is zero, bandwidth is infinite | Production failures: timeouts, retries storm, partial failures | Design for failure: circuit breakers, bulkheads, retries with exponential backoff, idempotency |
Distributed Monolith Example
graph LR
A[Order Service v1.2] -->|Requires| B[Inventory Service v1.2]
B -->|Requires| C[Payment Service v1.2]
C -->|Requires| D[Shipping Service v1.2]
style A fill:#FF6B6B
style B fill:#FF6B6B
style C fill:#FF6B6B
style D fill:#FF6B6B
Problem: Services are separate, but API changes in Inventory v1.3 break Order. Must coordinate deployment of all 4 services. Fix: Use API versioning so Order v1.2 can call Inventory v1.3 (backward compatible).
Interview Question: What is a distributed monolith?
Strong Answer: "A distributed monolith is when you have microservices architecture—multiple deployable units—but they're so tightly coupled that you can't deploy them independently. For example, if Order service breaks when Inventory service updates its API, and you're forced to deploy both together, you've lost the main benefit of microservices (deployment independence) while taking on all the distributed system complexity. This usually happens from poor API versioning or synchronous coupling. The fix is to use API versioning, async events for decoupling, and contract testing to catch breaking changes early."
11. Migration Strategy
Strangler Fig Pattern (Step-by-Step)
graph TD
A[Identify Bounded Context to Extract] --> B[Product Catalog: high read load, independent lifecycle]
B --> C[Build New Microservice with Same API]
C --> D[Deploy Side-by-Side with Monolith]
D --> E[Route 10% Traffic via API Gateway]
E --> F{Monitor Errors, Latency}
F -->|Issues| G[Rollback to 0%, Fix Microservice]
F -->|Success| H[Increase to 50%, then 100%]
H --> I[Remove Code from Monolith]
I --> J[Repeat for Next Context]
Phase 1: Preparation
| Step | Action | Deliverable |
|---|---|---|
| 1. Audit Monolith | Identify bounded contexts, dependencies, shared DB tables | Context map diagram, dependency graph |
| 2. Prioritize Extraction | Which context has highest pain? (Scaling, deployment frequency, team ownership) | Ranked list. Start with high-value, low-risk (e.g., read-heavy catalog, not core checkout) |
| 3. Set Up Platform | CI/CD, K8s, observability (Prometheus, Jaeger), API gateway | Ability to deploy, monitor, and route traffic to microservices |
Phase 2: Extract First Service
| Step | Action | Example (Product Catalog) |
|---|---|---|
| 4. Create Service Skeleton | New repo, same API as monolith (REST endpoints) | product-service: GET /products/:id, POST /products |
| 5. Migrate Data | Copy products table to new DB. Use CDC (Debezium) for real-time sync during migration. | Monolith writes to products, CDC streams to product-service DB |
| 6. Deploy Parallel | Monolith and microservice both running | API Gateway routes 0% to product-service (shadow mode: log but don't serve) |
| 7. Canary Rollout | 10% traffic to microservice | Monitor error rate, latency P99. Compare microservice vs monolith metrics. |
| 8. Ramp to 100% | Increase gradually: 10% → 50% → 100% | Once 100%, stop CDC sync, remove monolith's product code |
Phase 3: Handle Dependencies
| Challenge | Solution |
|---|---|
| Monolith calls extracted service | Monolith's Order module needs Product data. Call product-service REST API (sync) or subscribe to ProductUpdated events (async). |
| Shared DB tables | Customers table used by both Order and Customer modules. Extract Customer service first, then Order service calls Customer API. |
| Transactions across monolith + microservice | No distributed transactions. Use Saga pattern. Example: Order creation spans monolith (order) + microservice (inventory). Implement as saga with compensation. |
Branch by Abstraction (Alternative)
- Introduce abstraction layer in monolith:
ProductRepositoryinterface - Implement
MonolithProductRepository(queries local DB) - Implement
MicroserviceProductRepository(calls product-service API) - Use feature flag to toggle: 10% use microservice, 90% use monolith
- Increase flag percentage, remove monolith implementation
Benefit: Gradual migration without API gateway changes. Risk: Two code paths to maintain during migration.
Parallel Run (for High-Risk Services)
sequenceDiagram
participant Client
participant API Gateway
participant Monolith
participant Payment Service
Client->>API Gateway: POST /payments
API Gateway->>Monolith: charge(card)
API Gateway->>Payment Service: charge(card) [shadow mode]
Monolith-->>API Gateway: Success
Payment Service-->>API Gateway: Success [logged, not returned]
API Gateway-->>Client: Success (from monolith)
Use Case: Payment processing too risky to canary. Run microservice in shadow mode, log/compare results, don't serve responses. After 1 week of identical results, switch to live traffic.
Interview Question: How would you migrate a monolith to microservices?
Strong Answer: "I'd use the strangler fig pattern. First, identify a bounded context with clear value—maybe the product catalog, which has high read load and changes frequently. Build a new product-service with the same REST API, deploy it alongside the monolith, and use an API gateway to route 10% of traffic to the microservice while 90% still goes to the monolith. Monitor error rates and latency, and if it looks good, gradually ramp to 100% over a week. Once fully migrated, remove the product code from the monolith. Then repeat for the next service. I'd avoid a big-bang rewrite—too risky and delays value delivery. Strangler fig lets you incrementally deliver and validate each service."
Chapter 31 Summary: Microservices Architecture
- Monolith vs Microservices: Microservices trade operational complexity for deployment independence and team autonomy. Start monolith for MVPs; extract services when scaling/team boundaries emerge.
- Decomposition: Use business capability or DDD subdomains (core/supporting/generic). Avoid nano-services and premature decomposition.
- Bounded Contexts: Align each service with one bounded context. Use anti-corruption layers and context mapping to integrate.
- Communication: Sync (REST/gRPC) for queries needing immediate responses. Async (events/messages) for fire-and-forget, decoupling, and preventing cascading failures.
- API Design: REST for external, gRPC for internal high-throughput, GraphQL for aggregation. Version APIs to allow independent deployment.
- Data Management: Database per service. No shared DB. Use event sourcing, CDC, or CQRS to sync denormalized data. Eventual consistency is acceptable.
- Distributed Transactions: Use Saga pattern (choreography for simple workflows, orchestration for complex). Idempotency for at-least-once delivery.
- Service Mesh: Provides mTLS, observability, traffic control via sidecar proxies. Use for 50+ services; overkill for small deployments.
- Testing: 50% unit, 25% contract, 20% integration, 5% E2E. Contract testing (Pact) prevents breaking API changes.
- Anti-Patterns: Distributed monolith (tight coupling), nano-services (too fine-grained), shared libraries with business logic, synchronous chains.
- Migration: Strangler fig pattern: extract one service at a time with canary rollout. Branch by abstraction for gradual code migration. Parallel run for high-risk services.
API Pagination Strategies
| Strategy | How It Works | Performance | Use Case | Limitations |
|---|---|---|---|---|
| Offset Pagination | LIMIT 20 OFFSET 100 — skip first N rows | O(N) — DB scans N rows even if skipped | Simple admin UIs, small datasets (<10K rows) | Degrades at scale, inconsistent on inserts (page drift) |
| Cursor-Based | WHERE id > last_seen_id LIMIT 20 — use index | O(1) — index seek, no skipping | Infinite scroll, real-time feeds (Twitter, Facebook) | Can't jump to page N, only next/prev |
| Keyset Pagination | WHERE (created_at, id) > (last_created_at, last_id) LIMIT 20 | O(1) with composite index | Sort by any column, API responses with arbitrary ordering | Composite index required, more complex SQL |
Offset Pagination Performance Degradation
| Offset | Query | Rows Scanned | Latency (10M row table) |
|---|---|---|---|
| 0 | LIMIT 20 OFFSET 0 | 20 | 5ms (index scan) |
| 100 | LIMIT 20 OFFSET 100 | 120 | 8ms (fast, cached) |
| 10,000 | LIMIT 20 OFFSET 10000 | 10,020 | 50ms (DB scans 10K rows, discards them) |
| 100,000 | LIMIT 20 OFFSET 100000 | 100,020 | 800ms (unacceptable) |
| 1,000,000 | LIMIT 20 OFFSET 1000000 | 1,000,020 | 12s (DB scans 1M rows to skip them) |
Problem: Database must scan and discard all offset rows before returning results. At 1M offset, DB reads 1M rows from disk, then throws them away. No amount of indexing fixes this—it's inherent to OFFSET.
Cursor-Based Pagination (Twitter/Facebook Graph API)
-- Initial request
SELECT * FROM posts WHERE user_id = 123 ORDER BY id DESC LIMIT 20
→ Returns posts [1000, 999, 998, ..., 981]
-- Next page (client sends last_seen_id = 981)
SELECT * FROM posts WHERE user_id = 123 AND id < 981 ORDER BY id DESC LIMIT 20
→ Returns posts [980, 979, ..., 961]
-- Previous page (client sends first_seen_id = 961)
SELECT * FROM posts WHERE user_id = 123 AND id > 961 ORDER BY id ASC LIMIT 20
→ Returns posts [962, 963, ..., 981], reverse order in client
Performance: O(1) with index on (user_id, id). DB seeks directly to id=981 via index, no scanning.
Benefit: Stable under inserts. If 10 new posts added, page 2 still shows same results (id < 981).
Limitation: Can't jump to page 50. Only next/prev. Fine for infinite scroll, bad for "Go to page N" UIs.
Keyset Pagination (Sort by Any Column)
-- Sort by created_at, paginate with composite cursor
SELECT * FROM orders
WHERE (created_at, id) > ('2026-05-01 10:00:00', 12345)
ORDER BY created_at, id
LIMIT 20
-- Response includes cursor for next page
{
"orders": [...],
"next_cursor": {
"created_at": "2026-05-01 10:15:00",
"id": 12365
}
}
Why composite cursor? created_at alone isn't unique. Two orders at same timestamp need secondary sort (id) for stable pagination.
Index required: CREATE INDEX idx_orders_created ON orders(created_at, id)
Benefit: Works with any sort column (price, name, etc.), still O(1) performance.
Comparison Table
| Dimension | Offset | Cursor-Based | Keyset |
|---|---|---|---|
| Performance at Scale | O(N) — degrades linearly with offset | O(1) — constant time | O(1) — constant time |
| Random Access (Jump to Page N) | Yes | No (only next/prev) | No (only next/prev) |
| Consistency on Inserts | Page drift: page 2 shifts if row added to page 1 | Stable: page 2 locked to id < cursor | Stable: page 2 locked to (created_at, id) > cursor |
| Complexity | Trivial: LIMIT N OFFSET M | Simple: WHERE id > cursor | Moderate: WHERE (col1, col2) > (c1, c2) |
| Sort Flexibility | Any column | Primary key only (id, created_at) | Any column with composite cursor |
| Use Case | Admin tools, small datasets, "Go to page 50" UI | Infinite scroll (Twitter, Instagram), mobile feeds | API responses with arbitrary sort (order by price, date, etc.) |
Real-World Example: Twitter Timeline
GET /api/tweets?user_id=123&limit=20
→ {
"tweets": [...],
"next_cursor": "1234567890" // max_id of last tweet
}
GET /api/tweets?user_id=123&limit=20&cursor=1234567890
→ Returns tweets with id < 1234567890
-- SQL query
SELECT * FROM tweets
WHERE user_id = 123 AND id < 1234567890
ORDER BY id DESC
LIMIT 20
Why Twitter uses cursor: Timeline has millions of tweets. OFFSET 100000 would scan 100K rows. Cursor pagination with (user_id, id) index is O(1) regardless of position.
Handling Deletes and Consistency
| Scenario | Offset Behavior | Cursor Behavior |
|---|---|---|
| Row deleted from page 1 | Page 2 shifts: last item from page 1 moves to page 2 (duplicate) | Stable: page 2 still shows id < cursor, no duplicates |
| Row inserted before cursor | Page 2 shifts: first item from page 2 moves to page 3 (skipped) | Stable: page 2 still shows id < cursor, new row appears on refresh |
| Cursor row deleted | N/A | Next page uses WHERE id < cursor. Deleted row doesn't matter—cursor is just a value, not a pointer. |
API Response Format (Cursor-Based)
// REST API with cursor pagination
GET /api/orders?limit=20
Response:
{
"data": [
{"id": 1000, "total": 50.00, "created_at": "2026-05-12T10:00:00Z"},
{"id": 999, "total": 75.50, "created_at": "2026-05-12T09:45:00Z"},
...
],
"pagination": {
"next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDUtMTJUMDk6MDA6MDBaIiwgImlkIjogOTgxfQ==",
"has_more": true
}
}
// Client decodes Base64 cursor → {"created_at": "2026-05-12T09:00:00Z", "id": 981}
GET /api/orders?limit=20&cursor=eyJjcmVhdGVkX2F0IjogIjIwMjYtMDUtMTJUMDk6MDA6MDBaIiwgImlkIjogOTgxfQ==
Why Base64 encode cursor? Hide implementation details (client shouldn't parse it). Allows changing cursor format without breaking clients.
Interview Question: How would you paginate a large dataset in a REST API?
Strong Answer: "I'd use cursor-based pagination, not offset. With offset pagination, LIMIT 20 OFFSET 100000 forces the database to scan 100,000 rows before returning results—performance degrades linearly with page number. In a 10M row table, deep pages take 10+ seconds. Cursor pagination uses WHERE id > last_seen_id LIMIT 20, which is O(1) with an index—constant time regardless of position. Twitter and Facebook use cursor pagination for timelines with millions of posts. The tradeoff is you can't jump to page 50—only next/prev—but that's fine for infinite scroll. For admin UIs where users need 'Go to page N', I'd stick with offset but warn about performance on deep pages or add a max offset (e.g., 10,000). Always mention cursor-based for infinite scroll in interviews—offset pagination at scale is a red flag."