Loading content...
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.
Let's first acknowledge the undeniable: State and Strategy patterns have nearly identical structures. Both involve:
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 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.
The Gang of Four book describes distinct intents for each pattern. These formal intent statements clarify when each pattern is appropriate:
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.
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 statesStrategy 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.
| 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 strategyState 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.
| 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)// DRAFTUsing 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).
Use this decision framework to choose between State and Strategy patterns:
If you're uncertain, sketch the behavior object relationships. If you draw arrows between them with transition labels, it's State. If you draw them as parallel alternatives with selection arrows from outside, it's Strategy.
State and Strategy aren't mutually exclusive. Complex systems often use both patterns together, each handling its appropriate concern:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// States use strategies for their internal algorithm choices interface PaymentState { processPayment(order: Order): void; getStateName(): string;} interface PaymentMethodStrategy { charge(amount: number): PaymentResult;} // States handle lifecycle, strategies handle algorithm choiceclass AwaitingPaymentState implements PaymentState { // State uses a strategy for HOW to process payment constructor(private paymentStrategy: PaymentMethodStrategy) {} getStateName(): string { return "AWAITING_PAYMENT"; } processPayment(order: Order): void { // Use strategy for the algorithm const result = this.paymentStrategy.charge(order.getTotal()); // State handles transitions based on result if (result.success) { order.setPaymentConfirmation(result.confirmationId); order.setState(new PaidState()); } else if (result.shouldRetry) { order.incrementRetryCount(); // Stay in same state but could change strategy order.setState(new AwaitingPaymentState( new FallbackPaymentStrategy() )); } else { order.setFailureReason(result.error); order.setState(new PaymentFailedState()); } }} // Strategies are pure algorithms for payment processingclass CreditCardStrategy implements PaymentMethodStrategy { charge(amount: number): PaymentResult { // Credit card processing logic return this.processWithGateway("stripe", amount); }} class PayPalStrategy implements PaymentMethodStrategy { charge(amount: number): PaymentResult { // PayPal processing logic return this.processWithPayPal(amount); }} // Usage shows both patterns working togetherconst order = new Order();const paymentStrategy = customer.preferredMethod === "PAYPAL" ? new PayPalStrategy() : new CreditCardStrategy(); order.setState(new AwaitingPaymentState(paymentStrategy));order.processPayment(); // State delegates to strategy, then transitionsState and Strategy patterns share structural DNA but serve fundamentally different purposes. Let's crystallize the key distinctions:
| Criterion | Use State | Use Strategy |
|---|---|---|
| Primary purpose | Manage lifecycle behavior | Encapsulate algorithms |
| Object relationship | Part of object's identity | Tool the object uses |
| Transition source | State decides | External code decides |
| Awareness | States know context deeply | Strategies are independent |
| Graph structure | Constrained state graph | Flat list of alternatives |
| Typical lifetime | Changes throughout object life | Set once, used repeatedly |
You've now completed the State Pattern module. You understand the problem of state-dependent behavior, the elegant solution of state objects, the mechanics of state transitions, and how to distinguish State from Strategy. Apply this pattern when your objects have lifecycles with distinct behavioral phases—and watch your conditional complexity melt away.