Lab 06: Microservices Patterns

Time: 30 minutes | Level: Advanced | Docker: docker run -it --rm node:20-alpine sh

Overview

Implement microservice patterns in Node.js: API Gateway, Circuit Breaker state machine, Bulkhead, Service Discovery, health checks, graceful shutdown, and structured logging.


Step 1: Circuit Breaker

// Circuit Breaker: CLOSED -> OPEN -> HALF_OPEN -> CLOSED/OPEN
class CircuitBreaker {
  static STATES = { CLOSED: 'CLOSED', OPEN: 'OPEN', HALF_OPEN: 'HALF_OPEN' };

  #fn; #state; #failures; #successes; #nextAttempt;
  #threshold; #successThreshold; #timeout; #onStateChange;

  constructor(fn, options = {}) {
    this.#fn = fn;
    this.#state = CircuitBreaker.STATES.CLOSED;
    this.#failures = 0;
    this.#successes = 0;
    this.#threshold = options.threshold ?? 5;
    this.#successThreshold = options.successThreshold ?? 2;
    this.#timeout = options.timeout ?? 30000;
    this.#onStateChange = options.onStateChange ?? (() => {});
    this.#nextAttempt = Date.now();
  }

  get state() { return this.#state; }
  get metrics() { return { state: this.#state, failures: this.#failures, successes: this.#successes }; }

  #transition(newState) {
    if (this.#state !== newState) {
      this.#onStateChange(this.#state, newState);
      this.#state = newState;
    }
  }

  async call(...args) {
    if (this.#state === CircuitBreaker.STATES.OPEN) {
      if (Date.now() < this.#nextAttempt) {
        throw new Error(`Circuit breaker OPEN. Retry after ${new Date(this.#nextAttempt).toISOString()}`);
      }
      this.#transition(CircuitBreaker.STATES.HALF_OPEN);
      this.#successes = 0;
    }

    try {
      const result = await this.#fn(...args);
      this.#onSuccess();
      return result;
    } catch (err) {
      this.#onFailure();
      throw err;
    }
  }

  #onSuccess() {
    this.#failures = 0;
    if (this.#state === CircuitBreaker.STATES.HALF_OPEN) {
      this.#successes++;
      if (this.#successes >= this.#successThreshold) {
        this.#transition(CircuitBreaker.STATES.CLOSED);
      }
    }
  }

  #onFailure() {
    this.#failures++;
    if (this.#failures >= this.#threshold || this.#state === CircuitBreaker.STATES.HALF_OPEN) {
      this.#nextAttempt = Date.now() + this.#timeout;
      this.#transition(CircuitBreaker.STATES.OPEN);
    }
  }
}

// Demo
let callCount = 0;
const unstableService = async () => {
  callCount++;
  if (callCount <= 3) throw new Error('Service down');
  return 'Success';
};

const cb = new CircuitBreaker(unstableService, {
  threshold: 3,
  onStateChange: (from, to) => console.log(`Circuit: ${from} -> ${to}`)
});

(async () => {
  for (let i = 0; i < 5; i++) {
    try {
      const r = await cb.call();
      console.log(`Call ${i+1}: ${r} [${cb.state}]`);
    } catch (e) {
      console.log(`Call ${i+1}: ${e.message.slice(0,30)}... [${cb.state}]`);
    }
  }
})();

Step 2: Bulkhead Pattern


Step 3: Retry Pattern


Step 4: Structured Logging


Step 5: Health Checks


Step 6: Graceful Shutdown


Step 7: Service Discovery (Simple)


Step 8: Capstone — Circuit Breaker State Machine

Run verification:

📸 Verified Output:


Summary

Pattern
Problem Solved
Implementation

Circuit Breaker

Prevent cascading failures

State machine: CLOSED/OPEN/HALF_OPEN

Bulkhead

Isolate resources

Semaphore + queue

Retry + Backoff

Transient failures

Exponential backoff + jitter

Structured logging

Observability

JSON log lines

Health check

Service monitoring

/health endpoint

Graceful shutdown

Clean termination

SIGTERM handler + cleanup

Service registry

Service discovery

Register/discover pattern

Last updated