Loading content...
Design patterns exist because they solve recurring problems in real software. The Gang of Four didn't invent patterns—they documented solutions that experienced developers had already discovered through practice. Understanding patterns in their natural habitat—production systems—cements comprehension far better than isolated examples.
This page examines how behavioral patterns appear in systems you likely use daily:
By seeing patterns in context, you'll develop the intuition to recognize opportunities for pattern application in your own work.
By the end of this page, you will recognize behavioral patterns in production systems, understand why particular patterns were chosen, and learn implementation strategies used at scale. These examples bridge textbook definitions with working software.
Text editors like VS Code, IntelliJ IDEA, and Sublime Text are pattern-rich environments. The complex interaction between editing, navigation, undo/redo, and extensibility requires multiple behavioral patterns working in concert.
Patterns in a Text Editor:
| Feature | Pattern(s) | Implementation Approach |
|---|---|---|
| Undo/Redo | Command + Memento | Commands capture edits; mementos capture document state snapshots |
| Multiple Cursors | Iterator | Iterator over cursor positions; unified editing operations |
| Find & Replace | Iterator + Strategy | Iterator traverses matches; strategy defines replacement logic |
| Syntax Highlighting | Visitor | Visitor traverses AST; applies highlighting rules to node types |
| Plugin System | Observer + Command | Plugins observe editor events; extend via command registration |
| Key Bindings | Command + Chain of Responsibility | Keys dispatch commands; handlers check context before execution |
| Editor Modes | State | Normal/Insert/Visual modes with mode-specific behavior |
Deep Dive: Command-Based Undo in VS Code
VS Code's undo system demonstrates Command + Memento in production. Every text edit creates a command object:
// Simplified VS Code edit command concept
interface IEditOperation {
range: IRange; // Where the edit applies
text: string; // What text to insert
forceMoveMarkers?: boolean;
}
interface ICommand {
getEditOperations(): IEditOperation[];
computeCursorState(): ICursorState;
}
Commands are executed and stored in an undo stack. Rather than computing inverses, VS Code uses a snapshot approach for complex operations—capturing document state before significant changes.
Cursor state preservation is particularly interesting: each command tracks where cursors should be after undo, ensuring a seamless user experience when undoing multi-cursor edits.
Open your favorite text editor and trace the patterns: Press Ctrl+Z (Command). Select a code block and refactor (Visitor on AST). Watch syntax highlighting update (Observer). Open command palette (Command + Chain of Responsibility). The patterns are everywhere once you know what to look for.
E-commerce platforms like Amazon, Shopify, and Stripe rely heavily on behavioral patterns for order lifecycle management, payment processing, and notification systems.
The Order Lifecycle Challenge:
Orders transition through multiple states: Created → Paid → Processing → Shipped → Delivered (or Cancelled/Refunded at various points). Each state has different valid operations, transition rules, and notifications.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
// State pattern for order lifecycleinterface OrderState { getName(): string; // Available operations vary by state pay(context: Order): void; cancel(context: Order): void; ship(context: Order): void; deliver(context: Order): void; refund(context: Order): void; // State-specific behavior getAvailableActions(): string[];} // Created state - awaiting paymentclass CreatedState implements OrderState { getName() { return 'CREATED'; } pay(context: Order): void { // Validate payment info if (context.validatePayment()) { context.processPayment(); context.setState(new PaidState()); context.notify('OrderPaid', { orderId: context.getId() }); } } cancel(context: Order): void { context.releaseInventory(); context.setState(new CancelledState()); context.notify('OrderCancelled', { orderId: context.getId() }); } ship(context: Order): void { throw new InvalidOperationError('Cannot ship unpaid order'); } deliver(context: Order): void { throw new InvalidOperationError('Cannot deliver unpaid order'); } refund(context: Order): void { throw new InvalidOperationError('Cannot refund unpaid order'); } getAvailableActions() { return ['pay', 'cancel']; }} // Paid state - ready for fulfillmentclass PaidState implements OrderState { getName() { return 'PAID'; } pay(context: Order): void { throw new InvalidOperationError('Order already paid'); } cancel(context: Order): void { // Full refund on cancellation context.processRefund(); context.releaseInventory(); context.setState(new CancelledState()); context.notify('OrderCancelled', { orderId: context.getId(), refunded: true }); } ship(context: Order): void { if (context.validateInventory()) { context.reserveInventory(); context.createShipment(); context.setState(new ShippedState()); context.notify('OrderShipped', { orderId: context.getId(), trackingNumber: context.getTrackingNumber() }); } } deliver(context: Order): void { throw new InvalidOperationError('Cannot deliver unshipped order'); } refund(context: Order): void { // Refund before shipping context.processRefund(); context.releaseInventory(); context.setState(new RefundedState()); context.notify('OrderRefunded', { orderId: context.getId() }); } getAvailableActions() { return ['cancel', 'ship', 'refund']; }} // Shipped state - in transitclass ShippedState implements OrderState { getName() { return 'SHIPPED'; } pay(context: Order): void { throw new InvalidOperationError('Order already paid'); } cancel(context: Order): void { throw new InvalidOperationError('Cannot cancel shipped order - use refund'); } ship(context: Order): void { throw new InvalidOperationError('Order already shipped'); } deliver(context: Order): void { context.confirmDelivery(); context.setState(new DeliveredState()); context.notify('OrderDelivered', { orderId: context.getId() }); } refund(context: Order): void { // Need to wait for return throw new InvalidOperationError('Return required before refund'); } getAvailableActions() { return ['deliver']; }} // Order context combines State + Observerclass Order { private state: OrderState = new CreatedState(); private observers: OrderObserver[] = []; // State pattern: delegate to current state pay() { this.state.pay(this); } cancel() { this.state.cancel(this); } ship() { this.state.ship(this); } deliver() { this.state.deliver(this); } refund() { this.state.refund(this); } setState(state: OrderState): void { console.log(`Order ${this.id}: ${this.state.getName()} → ${state.getName()}`); this.state = state; } // Observer pattern: notification subscribe(observer: OrderObserver): void { this.observers.push(observer); } notify(event: string, data: object): void { this.observers.forEach(o => o.onOrderEvent(event, data)); } // Internal operations validatePayment(): boolean { /* ... */ return true; } processPayment(): void { /* payment gateway call */ } processRefund(): void { /* refund via gateway */ } // ... other internal methods}Pattern Synergy in Order Processing:
| Pattern | Role | Benefit |
|---|---|---|
| State | Manages order lifecycle | Eliminates complex conditional logic |
| Observer | Notifies subscribed services | Decouples order from notification targets |
| Strategy | Selects shipping/payment providers | Runtime provider selection |
| Command | Encapsulates order operations | Enables audit logging, async processing |
Production Considerations:
Real e-commerce systems add additional sophistication:
GUI frameworks like React, Angular, and native platforms (iOS UIKit, Android) are built on behavioral patterns, particularly Observer for event handling and State for component behavior.
The Observer Pattern in GUI Frameworks:
Every modern UI framework implements some form of the Observer pattern for reactive updates. The terminology varies—React calls it "state and effects," Angular uses "change detection," and mobile frameworks use "delegates" and "observers"—but the underlying pattern is consistent.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// Observer Pattern: useState triggers re-render "observers"function ShoppingCart() { // State change notifies React's reconciler (observer) const [items, setItems] = useState<CartItem[]>([]); const [total, setTotal] = useState(0); // Strategy Pattern: calculation strategies const discountStrategies = { none: (total: number) => total, percentage: (total: number) => total * 0.9, fixed: (total: number) => Math.max(0, total - 20), }; const [discountStrategy, setDiscountStrategy] = useState<keyof typeof discountStrategies>('none'); // Strategy selection at runtime const calculateTotal = () => { const subtotal = items.reduce((sum, item) => sum + item.price, 0); return discountStrategies[discountStrategy](subtotal); }; // Command Pattern: actions that can be logged/undone const cartCommands = { addItem: (item: CartItem) => { console.log(`[Command] ADD_ITEM: ${item.name}`); setItems(prev => [...prev, item]); }, removeItem: (itemId: string) => { console.log(`[Command] REMOVE_ITEM: ${itemId}`); setItems(prev => prev.filter(i => i.id !== itemId)); }, applyDiscount: (strategy: keyof typeof discountStrategies) => { console.log(`[Command] APPLY_DISCOUNT: ${strategy}`); setDiscountStrategy(strategy); }, }; return (/* component JSX */);} // State Pattern: component modestype EditorMode = 'view' | 'edit' | 'preview'; function DocumentEditor() { const [mode, setMode] = useState<EditorMode>('view'); // State-specific rendering const renderContent = () => { switch (mode) { case 'view': return <DocumentViewer {...props} />; case 'edit': return <DocumentEditor {...props} />; case 'preview': return <DocumentPreview {...props} />; } }; // State transitions const handleModeChange = (newMode: EditorMode) => { // Validation before transition if (mode === 'edit' && hasUnsavedChanges) { confirmSave().then(() => setMode(newMode)); } else { setMode(newMode); } }; return (/* component JSX */);}Notice how each framework uses different terminology for the same patterns: React's 'state' and 'effects', Angular's 'Observables' and 'change detection', SwiftUI's '@Published' and 'Combine'. The underlying Observer pattern is identical—only the API surface differs.
Web frameworks like Express.js, ASP.NET Core, Django, and Spring Boot extensively use the Chain of Responsibility pattern for request processing. This pattern enables modular, composable request handling.
The Middleware Pattern:
Middleware represents a clean implementation of Chain of Responsibility where each handler:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
// Express.js style middleware (Chain of Responsibility)type NextFunction = () => Promise<void>;type Middleware = (ctx: Context, next: NextFunction) => Promise<void>; class Context { request: Request; response: Response; state: Record<string, unknown> = {}; constructor(req: Request, res: Response) { this.request = req; this.response = res; }} // Middleware pipeline (Chain of Responsibility implementation)class MiddlewarePipeline { private middlewares: Middleware[] = []; use(middleware: Middleware): this { this.middlewares.push(middleware); return this; } async handle(ctx: Context): Promise<void> { const dispatch = async (index: number): Promise<void> => { if (index >= this.middlewares.length) { return; // End of chain } const middleware = this.middlewares[index]; await middleware(ctx, () => dispatch(index + 1)); }; await dispatch(0); }} // Real-world middleware examples // 1. Logging Middleware (Observer-like: logs all requests)const loggingMiddleware: Middleware = async (ctx, next) => { const start = Date.now(); console.log(`→ ${ctx.request.method} ${ctx.request.url}`); await next(); // Continue chain const duration = Date.now() - start; console.log(`← ${ctx.response.status} (${duration}ms)`);}; // 2. Authentication Middleware (may short-circuit chain)const authMiddleware: Middleware = async (ctx, next) => { const token = ctx.request.headers['authorization']; if (!token) { ctx.response.status = 401; ctx.response.body = { error: 'Authentication required' }; return; // Short-circuit: don't call next() } try { const user = await verifyToken(token); ctx.state.user = user; // Add to context for later middleware await next(); } catch (error) { ctx.response.status = 401; ctx.response.body = { error: 'Invalid token' }; }}; // 3. Rate Limiting Middleware (may short-circuit)const rateLimitMiddleware: Middleware = async (ctx, next) => { const clientIp = ctx.request.ip; const requestCount = await redis.incr(`ratelimit:${clientIp}`); if (requestCount === 1) { await redis.expire(`ratelimit:${clientIp}`, 60); } if (requestCount > 100) { ctx.response.status = 429; ctx.response.body = { error: 'Too many requests' }; return; // Short-circuit } ctx.response.headers['X-RateLimit-Remaining'] = String(100 - requestCount); await next();}; // 4. Error Handling Middleware (wraps chain)const errorHandlerMiddleware: Middleware = async (ctx, next) => { try { await next(); } catch (error) { console.error('Unhandled error:', error); ctx.response.status = 500; ctx.response.body = { error: 'Internal server error' }; // Could notify error tracking service (Observer pattern) errorTracker.captureException(error); }}; // 5. Compression Middleware (post-processes response)const compressionMiddleware: Middleware = async (ctx, next) => { await next(); // First, let downstream generate response // Then compress if appropriate if (ctx.response.body && ctx.request.headers['accept-encoding']?.includes('gzip')) { ctx.response.body = await gzip(ctx.response.body); ctx.response.headers['content-encoding'] = 'gzip'; }}; // Build the pipelineconst app = new MiddlewarePipeline(); app .use(errorHandlerMiddleware) // First: catches all errors .use(loggingMiddleware) // Logs entry/exit .use(compressionMiddleware) // Compresses responses .use(rateLimitMiddleware) // Rate limits .use(authMiddleware) // Authenticates .use(routerMiddleware); // Finally: handles request // Handle incoming requestapp.handle(new Context(request, response));Complex business processes—insurance claims, loan approvals, hiring pipelines—are modeled as state machines. The State pattern, often combined with Observer and Command, powers workflow engines used across industries.
Finite State Machine Example: Insurance Claim Processing
An insurance claim moves through defined states, with specific transitions triggered by events:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
// Generic state machine frameworkinterface State<TContext, TEvent> { readonly name: string; onEnter?(context: TContext): void; onExit?(context: TContext): void; handle(context: TContext, event: TEvent): State<TContext, TEvent> | null;} interface StateMachine<TContext, TEvent> { currentState: State<TContext, TEvent>; context: TContext; transition(event: TEvent): void; subscribe(observer: StateObserver<TContext, TEvent>): void;} interface StateObserver<TContext, TEvent> { onTransition(from: State<TContext, TEvent>, to: State<TContext, TEvent>, context: TContext): void;} // Insurance claim contextinterface ClaimContext { claimId: string; policyId: string; claimAmount: number; submittedAt: Date; documents: Document[]; adjustorId?: string; reviewNotes?: string; approvalAmount?: number; denialReason?: string;} // Event typestype ClaimEvent = | { type: 'SUBMIT' } | { type: 'ASSIGN_ADJUSTOR'; adjustorId: string } | { type: 'REQUEST_INFO'; info: string } | { type: 'PROVIDE_INFO'; documents: Document[] } | { type: 'APPROVE'; amount: number } | { type: 'DENY'; reason: string } | { type: 'APPEAL' } | { type: 'CLOSE' }; // Concrete statesclass DraftState implements State<ClaimContext, ClaimEvent> { readonly name = 'DRAFT'; handle(context: ClaimContext, event: ClaimEvent): State<ClaimContext, ClaimEvent> | null { if (event.type === 'SUBMIT') { if (context.documents.length === 0) { throw new Error('Cannot submit claim without documents'); } context.submittedAt = new Date(); return new SubmittedState(); } return null; // Event not handled }} class SubmittedState implements State<ClaimContext, ClaimEvent> { readonly name = 'SUBMITTED'; onEnter(context: ClaimContext): void { console.log(`Claim ${context.claimId} submitted for review`); // Could trigger notifications via Observer } handle(context: ClaimContext, event: ClaimEvent): State<ClaimContext, ClaimEvent> | null { if (event.type === 'ASSIGN_ADJUSTOR') { context.adjustorId = event.adjustorId; return new UnderReviewState(); } return null; }} class UnderReviewState implements State<ClaimContext, ClaimEvent> { readonly name = 'UNDER_REVIEW'; handle(context: ClaimContext, event: ClaimEvent): State<ClaimContext, ClaimEvent> | null { switch (event.type) { case 'REQUEST_INFO': return new PendingInfoState(); case 'APPROVE': context.approvalAmount = event.amount; return new ApprovedState(); case 'DENY': context.denialReason = event.reason; return new DeniedState(); default: return null; } }} class PendingInfoState implements State<ClaimContext, ClaimEvent> { readonly name = 'PENDING_INFO'; handle(context: ClaimContext, event: ClaimEvent): State<ClaimContext, ClaimEvent> | null { if (event.type === 'PROVIDE_INFO') { context.documents.push(...event.documents); return new UnderReviewState(); } return null; }} class ApprovedState implements State<ClaimContext, ClaimEvent> { readonly name = 'APPROVED'; onEnter(context: ClaimContext): void { console.log(`Claim ${context.claimId} approved for $${context.approvalAmount}`); // Trigger payment process } handle(context: ClaimContext, event: ClaimEvent): State<ClaimContext, ClaimEvent> | null { if (event.type === 'CLOSE') { return new ClosedState(); } return null; }} class DeniedState implements State<ClaimContext, ClaimEvent> { readonly name = 'DENIED'; handle(context: ClaimContext, event: ClaimEvent): State<ClaimContext, ClaimEvent> | null { if (event.type === 'APPEAL') { return new UnderReviewState(); } if (event.type === 'CLOSE') { return new ClosedState(); } return null; }} class ClosedState implements State<ClaimContext, ClaimEvent> { readonly name = 'CLOSED'; onEnter(context: ClaimContext): void { console.log(`Claim ${context.claimId} closed`); } handle(context: ClaimContext, event: ClaimEvent): State<ClaimContext, ClaimEvent> | null { return null; // Terminal state - no transitions }} // State machine implementationclass ClaimStateMachine implements StateMachine<ClaimContext, ClaimEvent> { currentState: State<ClaimContext, ClaimEvent>; private observers: StateObserver<ClaimContext, ClaimEvent>[] = []; constructor(public context: ClaimContext) { this.currentState = new DraftState(); } transition(event: ClaimEvent): void { const nextState = this.currentState.handle(this.context, event); if (nextState) { const previousState = this.currentState; // Exit current state previousState.onExit?.(this.context); // Enter new state this.currentState = nextState; this.currentState.onEnter?.(this.context); // Notify observers (Observer pattern) this.observers.forEach(o => o.onTransition(previousState, nextState, this.context) ); } } subscribe(observer: StateObserver<ClaimContext, ClaimEvent>): void { this.observers.push(observer); }}Production systems often use state machine libraries like XState (JavaScript), Stateless (.NET), or Spring State Machine (Java). These provide visualization tools, persistence, and distributed state machine support built on the same patterns.
Modern distributed systems use event-driven architectures (EDA) where services communicate through events rather than direct calls. The Observer pattern scales to distributed systems through message brokers like Kafka, RabbitMQ, and AWS SNS/SQS.
Distributed Observer Pattern:
In a monolith, observers register directly with subjects. In distributed systems, a message broker acts as the intermediary, enabling:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
// Domain Events (Command + Event pattern)interface DomainEvent { eventId: string; eventType: string; occurredAt: Date; aggregateId: string; aggregateType: string; payload: unknown;} // Concrete eventsinterface OrderPlacedEvent extends DomainEvent { eventType: 'OrderPlaced'; payload: { orderId: string; userId: string; items: OrderItem[]; totalAmount: number; };} interface PaymentReceivedEvent extends DomainEvent { eventType: 'PaymentReceived'; payload: { orderId: string; paymentId: string; amount: number; method: string; };} // Event Publisher (Subject in Observer pattern)interface EventPublisher { publish(event: DomainEvent): Promise<void>;} class KafkaEventPublisher implements EventPublisher { constructor(private kafka: KafkaProducer) {} async publish(event: DomainEvent): Promise<void> { await this.kafka.send({ topic: `domain-events.${event.aggregateType}`, messages: [{ key: event.aggregateId, value: JSON.stringify(event), headers: { 'event-type': event.eventType, 'correlation-id': getCorrelationId(), }, }], }); }} // Event Handler (Observer in Observer pattern)interface EventHandler<T extends DomainEvent> { eventType: string; handle(event: T): Promise<void>;} // Concrete handlers - each service subscribes to relevant events // Inventory Service subscribes to OrderPlacedclass ReserveInventoryHandler implements EventHandler<OrderPlacedEvent> { eventType = 'OrderPlaced'; async handle(event: OrderPlacedEvent): Promise<void> { const { orderId, items } = event.payload; for (const item of items) { await this.inventoryService.reserve(item.productId, item.quantity, orderId); } await this.eventPublisher.publish({ eventType: 'InventoryReserved', aggregateId: orderId, // ... }); }} // Notification Service subscribes to OrderPlacedclass SendOrderConfirmationHandler implements EventHandler<OrderPlacedEvent> { eventType = 'OrderPlaced'; async handle(event: OrderPlacedEvent): Promise<void> { const { userId, orderId, totalAmount } = event.payload; const user = await this.userService.getUser(userId); await this.emailService.send({ to: user.email, template: 'order-confirmation', data: { orderId, totalAmount }, }); }} // Analytics Service subscribes to OrderPlacedclass TrackOrderHandler implements EventHandler<OrderPlacedEvent> { eventType = 'OrderPlaced'; async handle(event: OrderPlacedEvent): Promise<void> { await this.analyticsService.track('order_placed', { orderId: event.payload.orderId, value: event.payload.totalAmount, itemCount: event.payload.items.length, }); }} // Shipping Service subscribes to PaymentReceivedclass CreateShipmentHandler implements EventHandler<PaymentReceivedEvent> { eventType = 'PaymentReceived'; async handle(event: PaymentReceivedEvent): Promise<void> { const order = await this.orderRepository.findById(event.payload.orderId); await this.shipmentService.createShipment({ orderId: order.id, address: order.shippingAddress, items: order.items, }); }} // Event Dispatcher routes events to handlersclass EventDispatcher { private handlers = new Map<string, EventHandler<DomainEvent>[]>(); register(handler: EventHandler<DomainEvent>): void { const existing = this.handlers.get(handler.eventType) || []; existing.push(handler); this.handlers.set(handler.eventType, existing); } async dispatch(event: DomainEvent): Promise<void> { const handlers = this.handlers.get(event.eventType) || []; // Parallel execution of all handlers await Promise.all( handlers.map(handler => handler.handle(event).catch(err => { // Handle failure (retry, dead-letter queue, etc.) console.error(`Handler failed for ${event.eventType}:`, err); this.handleFailure(event, handler, err); }) ) ); } private handleFailure(event: DomainEvent, handler: EventHandler<DomainEvent>, error: Error): void { // Send to dead-letter queue for manual processing // Or schedule retry with exponential backoff }}Pattern Synergy in Event-Driven Architecture:
| Pattern | Role | Distributed Implementation |
|---|---|---|
| Observer | Notification distribution | Message broker with pub/sub |
| Command | Operation encapsulation | Domain events as messages |
| Chain of Responsibility | Event processing pipeline | Middleware/interceptors |
| Strategy | Handler selection | Router configurations |
| Mediator | Orchestration | Saga coordinators |
We've explored behavioral patterns in their natural habitat—production systems solving real problems. These examples demonstrate that design patterns aren't academic exercises; they're proven solutions embedded in software you use daily.
| Domain | Primary Patterns | Why These Patterns |
|---|---|---|
| Text Editors | Command, Memento, Observer, Visitor | Undo, events, AST operations |
| E-Commerce | State, Observer, Strategy, Command | Lifecycle, notifications, pricing, operations |
| GUI Frameworks | Observer, State, Strategy, Command | Reactivity, modes, rendering, actions |
| Web Frameworks | Chain of Responsibility, Command | Middleware, request handling |
| Workflow Engines | State, Observer, Command | Processes, events, actions |
| Event Systems | Observer, Command, Mediator | Pub/sub, messages, orchestration |
Conclusion: The Module Complete
You've now mastered the art of choosing behavioral patterns:
This knowledge equips you to analyze design problems, select appropriate patterns, and implement solutions that follow industry-proven approaches. The patterns aren't just theory—they're the foundation of the software ecosystem.
Congratulations! You've completed the module on Choosing the Right Behavioral Pattern. You now have the knowledge and frameworks to confidently select and combine behavioral patterns for any design challenge. Continue practicing pattern recognition in the codebases you work with daily.