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

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
  1. Identify bounded context to extract (e.g., catalog)
  2. Build new microservice with same API
  3. Route traffic via gateway: 10% → microservice, 90% → monolith
  4. Gradually increase percentage, monitor, rollback if issues
  5. 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:

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:

  1. Every pod gets an Envoy sidecar proxy injected
  2. Service A sends request to localhost:80 (sidecar intercepts via iptables)
  3. Sidecar A establishes mTLS connection to Sidecar B, applies retries/timeouts/circuit breaker
  4. Sidecar B forwards to Service B on localhost
  5. 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

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)

  1. Introduce abstraction layer in monolith: ProductRepository interface
  2. Implement MonolithProductRepository (queries local DB)
  3. Implement MicroserviceProductRepository (calls product-service API)
  4. Use feature flag to toggle: 10% use microservice, 90% use monolith
  5. 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

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."