State Pattern
State vs Strategy
Read aloud is unavailable in this browser.
Two Patterns, One Structure, Different Purposes
If you examine the class diagrams for the State Pattern and the Strategy Pattern, you'll notice something striking: they're nearly identical. Both patterns use composition to delegate behavior to an object implementing a common interface. Both allow the object to be swapped at runtime. Both achieve polymorphic behavior without inheritance.
Yet these patterns solve fundamentally different problems and should be applied in completely different situations. Confusing them leads to awkward designs where patterns are forced into ill-fitting roles.
This page clarifies the distinction through careful analysis of intent, behavior, and appropriate usage scenarios—ensuring you always select the right pattern for the problem at hand.
By the end of this page, you will understand the fundamental difference between State and Strategy patterns despite their structural similarity. You'll learn to distinguish them based on intent, object awareness, transition patterns, and usage scenarios. You'll see concrete examples where each pattern is appropriate and understand why using the wrong pattern creates design problems.
Structural Similarity
Let's first acknowledge the undeniable: State and Strategy patterns have nearly identical structures. Both involve:
- A Context class that contains a reference to an interface
- An Interface/Abstract Class that defines operations
- Concrete implementations of that interface
- Runtime swapping of the implementation
Visually comparing the class diagrams reinforces this similarity:
The structures are so similar that some developers consider them variations of the same pattern. However, this view misses the crucial differences in intent, behavior, and dynamics that make each pattern distinct.
Design patterns are defined not just by structure but by intent. Two patterns can have identical structures while solving completely different problems. This is why understanding the 'why' behind patterns is as important as understanding the 'how'.
The Fundamental Distinction
The core difference between State and Strategy can be distilled into a single principle:
Strategy Pattern — The context or client explicitly chooses which strategy to use based on external requirements. The strategy itself is passive and unaware of other strategies.
State Pattern — The states themselves determine transitions. State objects are aware they're part of a state machine and actively participate in state changes.
This distinction has profound implications for how the patterns behave at runtime.
| Aspect | Strategy Pattern | State Pattern |
|---|---|---|
| Who switches? | External code or client | The state objects themselves |
| Switching trigger | Explicit decision by caller | Internal transitions from state logic |
| Objects aware of alternates? | No, strategies don't know about each other | Yes, states often transition to specific other states |
| Switching frequency | Typically once (configuration) | Frequently throughout object lifecycle |
| Switching pattern | Selection from menu of options | Graph of connected states with rules |
12345678910111213141516171819202122232425
// Strategy: CLIENT decides which to useclass PaymentProcessor { private strategy: PaymentStrategy; setStrategy(strategy: PaymentStrategy) { this.strategy = strategy; } processPayment(amount: number) { return this.strategy.process(amount); }} // Client selects based on user choiceconst processor = new PaymentProcessor(); if (userChoice === "CREDIT_CARD") { processor.setStrategy(new CreditCardStrategy());} else if (userChoice === "PAYPAL") { processor.setStrategy(new PayPalStrategy());} else { processor.setStrategy(new CryptoStrategy());} processor.processPayment(100);12345678910111213141516171819202122232425262728
// State: STATES decide when to switchclass Order { private state: OrderState; // State is changed by states, not clients setState(state: OrderState) { this.state = state; } pay() { this.state.pay(this); }} class PendingPaymentState implements OrderState { pay(order: Order) { // STATE decides next state if (paymentSucceeds()) { order.setState(new PaidState()); } else { order.setState(new PaymentFailedState()); } }} // Client just triggers actionsconst order = new Order();order.pay(); // State handles transitionAsk yourself: 'Does the behavior object need to change the context's behavior object?' If yes, you need State. If no, you probably need Strategy. State objects participate in determining their successors; Strategy objects are passive algorithms selected by others.
Intent and Purpose
The Gang of Four book describes distinct intents for each pattern. These formal intent statements clarify when each pattern is appropriate:
- •"Define a family of algorithms, encapsulate each one, and make them interchangeable."
- •Strategies let the algorithm vary independently from clients that use it.
- •Purpose: Provide options for HOW to do something.
- •Context: The client has a choice of approaches and selects based on requirements.
- •Example: Different compression algorithms, sorting strategies, pricing calculations.
- •"Allow an object to alter its behavior when its internal state changes. The object will appear to change its class."
- •State pattern encapsulates state-specific behavior.
- •Purpose: Enable behavior to change based on WHAT the object currently is.
- •Context: The object undergoes lifecycle changes with distinct behavioral phases.
- •Example: Connection states, order lifecycle, document workflow.
The analogy that clarifies:
Strategy is like choosing how to travel to work: car, bus, bike, or walk. You pick the method that suits today's conditions—weather, schedule, distance. The methods are alternatives, and you consciously select one.
State is like the stages of a project: planning, development, testing, deployment. You don't choose to be in 'testing'—you enter testing when development completes. States follow each other according to rules, not choices.
The phrase 'appear to change its class' in State's intent is key. A connection in ESTABLISHED state behaves so differently from one in CLOSED state that it seems like a different object. Strategy doesn't create this illusion—it's the same object using different algorithms.
Behavioral Differences in Detail
Let's examine the behavioral differences more deeply with side-by-side comparisons:
| Behavioral Aspect | Strategy | State |
|---|---|---|
| Object lifecycle | Strategy typically set once and used throughout | State changes frequently throughout object's life |
| State machine | No state machine concept | Implicit or explicit state machine with transitions |
| Awareness | Strategy unaware of context details | State deeply coupled to context, knows its internals |
| Transition constraints | Any strategy can replace any other | Transitions follow rules (not all states reachable from all states) |
| Mutual exclusivity | Strategies are independent alternatives | States are sequential phases (usually) |
| Interface scope | Typically single focused algorithm | Often mirrors context's full interface |
| Context reference | Usually not needed (receives input, returns output) | Usually required (needs to trigger transitions, access data) |
| Reusability | High—same strategy across different contexts | Lower—states often specific to one context type |
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// Strategies are independent algorithms// They don't know or care about the context's state interface CompressionStrategy { compress(data: Buffer): Buffer; decompress(data: Buffer): Buffer;} class GzipStrategy implements CompressionStrategy { compress(data: Buffer): Buffer { // Pure algorithm - no context needed return zlib.gzipSync(data); } decompress(data: Buffer): Buffer { return zlib.gunzipSync(data); }} class LzmaStrategy implements CompressionStrategy { compress(data: Buffer): Buffer { return lzma.compress(data); } decompress(data: Buffer): Buffer { return lzma.decompress(data); }} // Strategies are interchangeable—any can replace any// No transitions, no state machine, no context couplingclass FileArchiver { private strategy: CompressionStrategy; // Client explicitly sets strategy setCompressionStrategy(strategy: CompressionStrategy) { this.strategy = strategy; } archive(files: File[]): Buffer { const combined = this.combineFiles(files); return this.strategy.compress(combined); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
// States are deeply coupled to context// They access context data and trigger transitions interface ConnectionState { open(conn: Connection): void; send(conn: Connection, data: Buffer): void; close(conn: Connection): void; receive(conn: Connection, data: Buffer): void;} class ClosedState implements ConnectionState { open(conn: Connection): void { // Access context, trigger transition conn.initiateHandshake(); conn.setState(new OpeningState()); } send(conn: Connection, data: Buffer): void { throw new Error("Cannot send: connection closed"); } close(conn: Connection): void { // Already closed—no-op or error } receive(conn: Connection, data: Buffer): void { // Ignore data in closed state }} class EstablishedState implements ConnectionState { open(conn: Connection): void { // Already open—no-op } send(conn: Connection, data: Buffer): void { conn.writeToSocket(data); // May transition to error state on failure } close(conn: Connection): void { conn.sendCloseFrame(); conn.setState(new ClosingState()); } receive(conn: Connection, data: Buffer): void { conn.enqueueReceivedData(data); if (isCloseFrame(data)) { conn.setState(new ClosingState()); } }} // States form a constrained graph// CLOSED → OPENING → ESTABLISHED → CLOSING → CLOSED// Not all states reachable from all statesWhen Strategy is Appropriate
Strategy Pattern is the right choice when you have alternative approaches to accomplish the same goal and want to select among them based on requirements, configuration, or user preference.
- •You have multiple algorithms that achieve the same outcome differently (sorting, compression, encryption, routing).
- •The choice is made externally — by configuration, user preference, or runtime conditions like data characteristics.
- •Algorithms are independent — they don't know about each other and don't form sequences.
- •The algorithm is pure — it transforms input to output without needing context state.
- •Algorithms are reusable — the same strategy can work with different contexts.
- •Switching is rare — typically at initialization or configuration, not continuously.
| Domain | Strategy Family | Concrete Strategies |
|---|---|---|
| Data Processing | Compression | Gzip, LZ4, Brotli, Snappy |
| Security | Encryption | AES, RSA, ChaCha20 |
| Pricing | Discount Calculation | Percentage, Fixed Amount, Buy-One-Get-One |
| Routing | Path Finding | Fastest, Shortest, Scenic, Avoid Tolls |
| Sorting | Sort Algorithm | QuickSort, MergeSort, TimSort |
| Validation | Input Validation | Strict, Lenient, Schema-Based |
| Rendering | Output Format | PDF, HTML, Markdown, Plain Text |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Different ways to calculate price - client choosesinterface PricingStrategy { calculateTotal(basePrice: number, quantity: number): number; getDescription(): string;} class RegularPricing implements PricingStrategy { calculateTotal(basePrice: number, quantity: number): number { return basePrice * quantity; } getDescription(): string { return "Standard pricing"; }} class BulkDiscountPricing implements PricingStrategy { calculateTotal(basePrice: number, quantity: number): number { const discount = quantity >= 100 ? 0.2 : quantity >= 50 ? 0.1 : 0; return basePrice * quantity * (1 - discount); } getDescription(): string { return "Bulk discount: 10% off 50+, 20% off 100+"; }} class SubscriberPricing implements PricingStrategy { private discountRate: number; constructor(subscriptionTier: string) { this.discountRate = tier === "GOLD" ? 0.25 : tier === "SILVER" ? 0.15 : 0.05; } calculateTotal(basePrice: number, quantity: number): number { return basePrice * quantity * (1 - this.discountRate); } getDescription(): string { return `Subscriber discount: ${this.discountRate * 100}% off`; }} // Usage: Client selects strategy based on customer typeconst cart = new ShoppingCart(); if (customer.isSubscriber()) { cart.setPricingStrategy(new SubscriberPricing(customer.subscriptionTier));} else if (cart.totalQuantity >= 50) { cart.setPricingStrategy(new BulkDiscountPricing());} else { cart.setPricingStrategy(new RegularPricing());} const total = cart.checkout(); // Uses selected strategyWhen State is Appropriate
State Pattern is the right choice when an object has a lifecycle with distinct phases, each with its own behavior, and transitions between phases follow defined rules.
- •Object has a lifecycle with distinct phases (created, active, suspended, terminated).
- •Same operations have different effects depending on current phase.
- •Transitions follow rules — not all states reachable from all states.
- •Transitions are triggered internally — states know when to transition.
- •States form a graph — there's a state diagram that makes sense.
- •The object appears to change — behavior is so different across states that it seems like a different object.
| Domain | Entity | States |
|---|---|---|
| E-commerce | Order | Created, Pending, Paid, Processing, Shipped, Delivered, Completed |
| Networking | Connection | Closed, Connecting, Connected, Closing |
| Documents | Workflow | Draft, Review, Approved, Published, Archived |
| Gaming | Character | Idle, Walking, Running, Jumping, Falling, Dead |
| Media | Player | Stopped, Playing, Paused, Buffering, Error |
| Banking | Account | Pending, Active, Frozen, Closed |
| Processes | Thread | New, Runnable, Running, Blocked, Terminated |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
// Document moves through lifecycle statesinterface DocumentState { edit(doc: Document, content: string): string; submit(doc: Document): string; approve(doc: Document, approver: User): string; reject(doc: Document, reason: string): string; publish(doc: Document): string;} class DraftState implements DocumentState { edit(doc: Document, content: string): string { doc.setContent(content); doc.setLastModified(new Date()); return "Content updated."; } submit(doc: Document): string { if (doc.getContent().length < 100) { return "Document too short for review."; } doc.setState(new PendingReviewState()); doc.notifyReviewers(); return "Document submitted for review."; } approve(doc: Document, approver: User): string { return "Cannot approve a draft. Submit for review first."; } reject(doc: Document, reason: string): string { return "Cannot reject a draft."; } publish(doc: Document): string { return "Cannot publish a draft. It must be approved first."; }} class PendingReviewState implements DocumentState { edit(doc: Document, content: string): string { return "Cannot edit during review. Withdraw and edit in draft."; } submit(doc: Document): string { return "Already submitted for review."; } approve(doc: Document, approver: User): string { if (!approver.hasApprovalAuthority()) { return "You don't have approval authority."; } doc.setApprover(approver); doc.setState(new ApprovedState()); return "Document approved!"; } reject(doc: Document, reason: string): string { doc.setRejectionReason(reason); doc.setState(new DraftState()); // Back to draft doc.notifyAuthor(reason); return "Document rejected. Author notified."; } publish(doc: Document): string { return "Cannot publish without approval."; }} class ApprovedState implements DocumentState { edit(doc: Document, content: string): string { return "Cannot edit approved document."; } submit(doc: Document): string { return "Already approved."; } approve(doc: Document, approver: User): string { return "Already approved."; } reject(doc: Document, reason: string): string { return "Cannot reject after approval."; } publish(doc: Document): string { doc.setPublishedAt(new Date()); doc.setState(new PublishedState()); doc.distributeToSubscribers(); return "Document published!"; }} // States manage their own transitions// DRAFT → PENDING_REVIEW → APPROVED → PUBLISHED// ↓ (reject)// DRAFTMisapplication Problems
Using the wrong pattern creates awkward code. Let's examine what happens when patterns are misapplied:
12345678910111213141516171819202122232425262728293031323334353637
// WRONG: Using Strategy for lifecycle states// This is awkward because states need to transition class OrderProcessor { private strategy: OrderStrategy; setStrategy(strategy: OrderStrategy) { this.strategy = strategy; } process() { this.strategy.process(this); }} // Problem 1: Who changes the strategy?// With Strategy, the client should... but the client doesn't know // when payment completes! // Problem 2: Strategies need context access for transitionsclass PendingPaymentStrategy implements OrderStrategy { process(processor: OrderProcessor) { // Wait for payment... if (paymentReceived()) { // Awkward! Strategy is changing strategy! processor.setStrategy(new ProcessingStrategy()); } }} // Problem 3: The naming is wrong// These aren't strategies (alternatives), they're phases (sequence)// Calling it "strategy" obscures the actual behavior // Problem 4: Testing is confusing// "Given a PendingPaymentStrategy, when ..."// No! It's not a strategy, it's a state!1234567891011121314151617181920212223242526272829303132333435363738
// WRONG: Using State for algorithm selection// This is awkward because algorithms don't transition interface CompressionState { compress(ctx: Compressor, data: Buffer): Buffer; switchToGzip(ctx: Compressor): void; // Awkward! switchToLzma(ctx: Compressor): void; // Awkward!} class GzipState implements CompressionState { compress(ctx: Compressor, data: Buffer): Buffer { return zlib.gzipSync(data); } // These methods are meaningless! // Compression algorithms don't "transition" switchToGzip(ctx: Compressor): void { // Already gzip... no-op? } switchToLzma(ctx: Compressor): void { // Why would gzip "transition" to lzma? // This isn't a lifecycle! ctx.setState(new LzmaState()); }} // Problem 1: No natural transition graph// Any algorithm can "transition" to any other—there's no constraint // Problem 2: Forced context coupling// States require context reference, but compression doesn't need it // Problem 3: Lifecycle semantics don't fit// onEnter/onExit for compression? That doesn't make sense // Problem 4: State machine overhead for simple selection// All we need is "client picks an algorithm"Pattern misapplication isn't just suboptimal—it actively harms code quality. It creates confusion (naming doesn't match behavior), adds unnecessary complexity (unused transition mechanics), and makes the code harder to understand (the pattern leads readers to wrong expectations).
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.