Siddhant Deval
Siddhant Deval
system design19 min read

Behavioral Patterns: State Machines, Responsibility Chains & Mediators

Complex domain lifecycles degrade under sprawling boolean flags. This article replaces conditional sprawl with formal Finite State Machines enforced by discriminated unions, composable Chain of Responsibility middleware pipelines with early bailout mechanics, and centralized Mediators that convert N×N inter-service meshes into a maintainable hub-and-spoke topology.

Behavioral Patterns: State Machines, Chains & Mediators

In TypeScript 5+, OOP is an architectural contract, not an inheritance tree: enforce domain invariants at compile time, encapsulate mutation strictly within aggregates, and invert dependencies so high-level business policy never couples to execution details. This article completes the behavioral pattern trilogy with the three patterns that manage flow control: State Machines govern which transitions are legal, Chains of Responsibility pipe a request through ordered middleware, and Mediators coordinate multiple aggregates without letting them reference each other.


1. The Anti-Pattern Graveyard: The Boolean Flag Explosion

Here is the state management pattern that every order system eventually produces:

TYPESCRIPT
// ❌ Anti-Pattern: Boolean flag state — transitions are implicit and unverifiable

class Order {
  isCreated: boolean = false;
  isPaid: boolean = false;
  isShipped: boolean = false;
  isCancelled: boolean = false;
  isRefunded: boolean = false;

  ship(): void {
    // The guard condition must check ALL possible invalid states manually
    if (!this.isCreated || !this.isPaid || this.isCancelled || this.isShipped) {
      throw new Error('Cannot ship in current state');
    }
    this.isShipped = true;
  }
}

// What states are valid? The caller must manually reconstruct the logic.
// What transitions are allowed from SHIPPED? Unknowable without reading all methods.
// Can an order be both isPaid and isCancelled? The type system has no opinion.

Five booleans produce 2^5 = 32 possible combinations. Most are illegal. The type system cannot model illegal combinations, so each method must defensively check all the others. Adding a new state (e.g., isOnHold) requires updating every method's guard condition.


2. Finite State Machine with Discriminated Unions

TypeScript's discriminated unions model finite state machines where each state is a structurally distinct type — illegal state combinations are impossible to construct.

2.1 The Order Lifecycle FSM

TYPESCRIPT
// Each state is a distinct type — cannot mix-and-match fields
type OrderState =
  | { status: 'DRAFT';     createdAt: Date }
  | { status: 'CONFIRMED'; confirmedAt: Date; totalAmountCents: number }
  | { status: 'PAID';      paidAt: Date; transactionId: string }
  | { status: 'SHIPPED';   shippedAt: Date; trackingCode: string }
  | { status: 'CANCELLED'; cancelledAt: Date; reason: string }
  | { status: 'REFUNDED';  refundedAt: Date; refundAmountCents: number };

// Allowed transitions — encoded as a type-level map
type Transitions = {
  DRAFT:     'confirm';
  CONFIRMED: 'pay' | 'cancel';
  PAID:      'ship' | 'refund';
  SHIPPED:   never;       // Terminal — no further transitions
  CANCELLED: never;       // Terminal
  REFUNDED:  never;       // Terminal
};

// Transition guards — only legal transitions compile
class OrderStateMachine {
  #state: OrderState;

  constructor() {
    this.#state = { status: 'DRAFT', createdAt: new Date() };
  }

  get state(): Readonly<OrderState> { return this.#state; }

  confirm(totalAmountCents: number): void {
    this.#assertStatus('DRAFT');
    this.#state = { status: 'CONFIRMED', confirmedAt: new Date(), totalAmountCents };
  }

  pay(transactionId: string): void {
    this.#assertStatus('CONFIRMED');
    this.#state = { status: 'PAID', paidAt: new Date(), transactionId };
  }

  ship(trackingCode: string): void {
    this.#assertStatus('PAID');
    this.#state = { status: 'SHIPPED', shippedAt: new Date(), trackingCode };
  }

  cancel(reason: string): void {
    const state = this.#state;
    if (state.status !== 'DRAFT' && state.status !== 'CONFIRMED') {
      throw new Error(`Cannot cancel from ${state.status}`);
    }
    this.#state = { status: 'CANCELLED', cancelledAt: new Date(), reason };
  }

  refund(refundAmountCents: number): void {
    this.#assertStatus('PAID');
    this.#state = { status: 'REFUNDED', refundedAt: new Date(), refundAmountCents };
  }

  #assertStatus<S extends OrderState['status']>(expected: S): asserts this is { '#state': Extract<OrderState, { status: S }> } {
    if (this.#state.status !== expected) {
      throw new Error(`Expected state ${expected}, got ${this.#state.status}`);
    }
  }
}

// Exhaustive state handler — compiler guarantees all states are covered
function describeOrderState(state: OrderState): string {
  switch (state.status) {
    case 'DRAFT':     return `Draft order created at ${state.createdAt.toISOString()}`;
    case 'CONFIRMED': return `Confirmed: $${state.totalAmountCents / 100}`;
    case 'PAID':      return `Paid: transaction ${state.transactionId}`;
    case 'SHIPPED':   return `Shipped: tracking ${state.trackingCode}`;
    case 'CANCELLED': return `Cancelled: ${state.reason}`;
    case 'REFUNDED':  return `Refunded: $${state.refundAmountCents / 100}`;
    // TypeScript ensures this is unreachable if all states are handled
  }
}

3. Chain of Responsibility: Payment Validation Middleware

The Chain of Responsibility pipes a request through a sequence of handlers. Each handler decides whether to process, enrich, or reject the request — without the client knowing how many handlers exist.

TYPESCRIPT
// Handler interface
interface IPaymentMiddleware {
  setNext(handler: IPaymentMiddleware): IPaymentMiddleware;
  handle(request: PaymentRequest): Promise<PaymentRequest>;
}

type PaymentRequest = {
  orderId: string;
  amountCents: number;
  currency: string;
  customerId: string;
  metadata: Record<string, string>;
  // Enriched by handlers:
  normalizedAmount?: number;
  fraudScore?: number;
};

// Abstract base handler — manages chain linkage
abstract class PaymentMiddleware implements IPaymentMiddleware {
  #next: IPaymentMiddleware | null = null;

  setNext(handler: IPaymentMiddleware): IPaymentMiddleware {
    this.#next = handler;
    return handler; // Enables chaining: a.setNext(b).setNext(c)
  }

  protected async passToNext(request: PaymentRequest): Promise<PaymentRequest> {
    if (this.#next) return this.#next.handle(request);
    return request; // End of chain — return as-is
  }

  abstract handle(request: PaymentRequest): Promise<PaymentRequest>;
}

// Concrete handlers
class AmountValidationMiddleware extends PaymentMiddleware {
  async handle(request: PaymentRequest): Promise<PaymentRequest> {
    if (request.amountCents <= 0) throw new Error('Amount must be positive');
    if (request.amountCents > 10_000_00) throw new Error('Amount exceeds maximum of $10,000');
    return this.passToNext(request); // Valid — pass to next handler
  }
}

class CurrencyNormalizationMiddleware extends PaymentMiddleware {
  async handle(request: PaymentRequest): Promise<PaymentRequest> {
    const rates: Record<string, number> = { USD: 1, EUR: 1.08, GBP: 1.27 };
    const rate = rates[request.currency];
    if (!rate) throw new Error(`Unsupported currency: ${request.currency}`);
    // Enrich the request before passing it on
    return this.passToNext({
      ...request,
      normalizedAmount: Math.round(request.amountCents * rate),
    });
  }
}

class FraudScoringMiddleware extends PaymentMiddleware {
  async handle(request: PaymentRequest): Promise<PaymentRequest> {
    const fraudScore = await this.computeFraudScore(request.customerId, request.amountCents);
    if (fraudScore > 80) throw new Error(`Payment blocked: fraud score ${fraudScore}`);
    return this.passToNext({ ...request, fraudScore });
  }

  private async computeFraudScore(customerId: string, amount: number): Promise<number> {
    // Simplified: high-value orders from new customers score higher
    return amount > 500_00 ? 40 : 10; // In reality: ML model call
  }
}

// Build the chain at the composition root
const chain = new AmountValidationMiddleware();
chain
  .setNext(new CurrencyNormalizationMiddleware())
  .setNext(new FraudScoringMiddleware());

// Process a request through the chain
const result = await chain.handle({
  orderId: 'ord_001',
  amountCents: 10_00, // $10.00
  currency: 'EUR',
  customerId: 'cus_abc',
  metadata: {},
});
// result.normalizedAmount = 1080 (€10 × 1.08)
// result.fraudScore = 10 (not high risk)

4. Mediator: Coordinating Aggregates Without Direct Coupling

The Mediator pattern introduces a central coordinator that manages interactions between components. Without it, each aggregate would need to reference every other aggregate it collaborates with — high coupling. With it, aggregates only reference the Mediator.

4.1 OrderFulfillmentMediator

TYPESCRIPT
// Mediator interface
interface IFulfillmentMediator {
  onOrderConfirmed(orderId: string, totalCents: number, customerId: string): Promise<void>;
  onPaymentCompleted(orderId: string, transactionId: string): Promise<void>;
  onShipmentCreated(orderId: string, trackingCode: string): Promise<void>;
}

// Concrete Mediator — coordinates all the aggregates
class OrderFulfillmentMediator implements IFulfillmentMediator {
  constructor(
    private readonly paymentGateway: IPaymentGateway,
    private readonly inventoryService: IInventoryService,
    private readonly notificationService: INotificationService,
  ) {}

  async onOrderConfirmed(orderId: string, totalCents: number, customerId: string): Promise<void> {
    // Coordinates payment + inventory reservation without either knowing about the other
    const result = await this.paymentGateway.charge(totalCents, 'USD', { orderId });
    if (result.status === 'FAILED') throw new Error(result.reason);

    await this.onPaymentCompleted(orderId, result.transactionId);
    await this.notificationService.send(customerId, `Order ${orderId} confirmed`);
  }

  async onPaymentCompleted(orderId: string, transactionId: string): Promise<void> {
    await this.inventoryService.commitReservation(orderId);
    console.log(`[Mediator] Payment ${transactionId} committed inventory for ${orderId}`);
  }

  async onShipmentCreated(orderId: string, trackingCode: string): Promise<void> {
    await this.notificationService.send('customer', `Your order ${orderId} shipped: ${trackingCode}`);
  }
}

interface IInventoryService {
  commitReservation(orderId: string): Promise<void>;
}
interface INotificationService {
  send(customerId: string, message: string): Promise<void>;
}
Architectural Note

Cross-Language Rosetta — State Machines:

  • Go: Typed constants + switch statement. No built-in FSM, but the pattern is identical: type OrderStatus string; const StatusDraft OrderStatus = "DRAFT".
  • Python: enum.Enum with @dataclass per state variant. Exhaustiveness via match (Python 3.10+).
  • TypeScript 5+: Discriminated union — each state variant is a structurally distinct type. Exhaustiveness via switch + optional assertNever.

5. Interview Synthesis: Combining All Three Patterns

A complete LLD interview answer for "Design an Order Management System":


Summary

Pattern Problem TypeScript 5+ Idiom
FSM (Discriminated Union) Boolean flag explosion — 2^N illegal states type State = | {status:'DRAFT'} | {status:'PAID'}
Chain of Responsibility Conditional validation spread across one method Abstract handler + setNext() + passToNext()
Mediator Aggregates import each other — high coupling Central IFulfillmentMediator interface
Exhaustive FSM handler Missing state transitions silently ignored switch(state.status) + assertNever

What's Next

In Part 12, the final article synthesizes the entire series into a complete LLD interview playbook — how to design a production fintech system from blank whiteboard to deployable TypeScript in 60 minutes, including time allocation, common probe questions, and the specific TypeScript 5+ patterns that signal senior-level architectural thinking to an interviewer. Part 12: Complete LLD Interview Playbook.

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#TypeScript#Design Patterns#State Machine#Chain of Responsibility#Mediator Pattern#LLD Interview
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.