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.
Here is the state management pattern that every order system eventually produces:
class Order {
isCreated: boolean = false;
isPaid: boolean = false;
isShipped: boolean = false;
isCancelled: boolean = false;
isRefunded: boolean = false;
ship(): void {
if (!this.isCreated || !this.isPaid || this.isCancelled || this.isShipped) {
throw new Error('Cannot ship in current state');
}
this.isShipped = true;
}
}
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.
TypeScript's discriminated unions model finite state machines where each state is a structurally distinct type — illegal state combinations are impossible to construct.
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 };
type Transitions = {
DRAFT: 'confirm';
CONFIRMED: 'pay' | 'cancel';
PAID: 'ship' | 'refund';
SHIPPED: never;
CANCELLED: never;
REFUNDED: never;
};
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}`);
}
}
}
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}`;
}
}
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.
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>;
normalizedAmount?: number;
fraudScore?: number;
};
abstract class PaymentMiddleware implements IPaymentMiddleware {
#next: IPaymentMiddleware | null = null;
setNext(handler: IPaymentMiddleware): IPaymentMiddleware {
this.#next = handler;
return handler;
}
protected async passToNext(request: PaymentRequest): Promise<PaymentRequest> {
if (this.#next) return this.#next.handle(request);
return request;
}
abstract handle(request: PaymentRequest): Promise<PaymentRequest>;
}
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);
}
}
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}`);
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> {
return amount > 500_00 ? 40 : 10;
}
}
const chain = new AmountValidationMiddleware();
chain
.setNext(new CurrencyNormalizationMiddleware())
.setNext(new FraudScoringMiddleware());
const result = await chain.handle({
orderId: 'ord_001',
amountCents: 10_00,
currency: 'EUR',
customerId: 'cus_abc',
metadata: {},
});
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.
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>;
}
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> {
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>;
}
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.
A complete LLD interview answer for "Design an Order Management System":
| 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 |
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.