Lab 13: Microservices Patterns

Time: 60 minutes | Level: Architect | Docker: docker run -it --rm node:20-alpine sh

Distributed systems fail in complex ways. This lab covers the essential resilience patterns: circuit breaker, bulkhead, retry, saga, event sourcing, CQRS, and outbox — using opossum, p-limit, and in-memory implementations.


Step 1: Install Dependencies

npm install opossum p-limit async-retry

Step 2: Circuit Breaker with opossum

The circuit breaker prevents cascading failures:

CLOSED → (failure threshold exceeded) → OPEN → (resetTimeout elapsed) → HALF-OPEN → (success) → CLOSED
                                                                                    → (failure) → OPEN
// file: circuit-breaker.mjs
import CircuitBreaker from 'opossum';

// Simulate an unreliable external service
async function callExternalService(requestId) {
  await new Promise(r => setTimeout(r, 10)); // simulate latency
  if (requestId % 3 === 0) throw new Error('Service temporarily unavailable');
  return { data: `result-${requestId}`, latency: 10 };
}

const breaker = new CircuitBreaker(callExternalService, {
  timeout: 3000,                  // fail if function takes > 3s
  errorThresholdPercentage: 50,   // open circuit after 50% failure rate
  resetTimeout: 1000,             // try again after 1s in OPEN state
  volumeThreshold: 4,             // minimum calls before evaluating
});

breaker.on('open',     () => console.log('⚡ Circuit OPEN  — requests blocked'));
breaker.on('halfOpen', () => console.log('🔄 Circuit HALF-OPEN — testing...'));
breaker.on('close',    () => console.log('✅ Circuit CLOSED — back to normal'));
breaker.on('reject',   () => console.log('🚫 Request REJECTED (circuit open)'));
breaker.on('timeout',  () => console.log('⏰ Request TIMED OUT'));
breaker.on('fallback', (result) => console.log('🔁 FALLBACK used:', result));

// Define fallback
breaker.fallback((reqId) => ({ data: 'cached-fallback', requestId: reqId }));

// Fire requests
for (let i = 1; i <= 8; i++) {
  try {
    const result = await breaker.fire(i);
    const state = breaker.opened ? 'OPEN' : breaker.halfOpen ? 'HALF-OPEN' : 'CLOSED';
    console.log(`  req ${i} ✓  state: ${state}  data: ${result.data}`);
  } catch (e) {
    const state = breaker.opened ? 'OPEN' : 'CLOSED';
    console.log(`  req ${i} ✗  state: ${state}  error: ${e.message}`);
  }
  await new Promise(r => setTimeout(r, 50));
}

console.log('\nStats:', breaker.stats);

📸 Verified Output:

💡 opossum uses a sliding window of the last N calls. Once errorThresholdPercentage is exceeded, the circuit opens and all calls immediately get the fallback response.


Step 3: Bulkhead Pattern with p-limit


Step 4: Retry with Exponential Backoff


Step 5: Saga Pattern — Compensating Transactions


Step 6: Event Sourcing — In-Memory Event Store


Step 7: CQRS — Command/Query Responsibility Segregation


Step 8: Capstone — Outbox Pattern for Reliable Event Publishing


Summary

Pattern
Library/Impl
Protects Against

Circuit Breaker

opossum

Cascading failures

Bulkhead

p-limit

Resource exhaustion

Retry + Backoff

custom / async-retry

Transient failures

Saga

custom

Distributed transaction rollback

Event Sourcing

custom EventStore

State loss, audit trail

CQRS

custom CommandBus/QueryBus

Read/write scaling

Outbox

custom DB+poll

Lost events on crash

Last updated