OOD Fundamentals
OOP Foundations
SOLID Principles
Creational Patterns
Structural Patterns
Classic OOD Problems: Part 1
Classic OOD Problems: Part 2
Strategy and State
When you need an object to do the same kind of task in multiple different ways, the Strategy pattern gives you a clean mechanism: define a family of algorithms, encapsulate each one in its own class, and make them interchangeable through a shared interface. The context object holds a reference to one strategy at a time and delegates the work to it. The client decides which strategy to plug in.
Why does this matter? Because the alternative is a growing pile of conditionals inside a single method. Every time a new algorithm appears, you crack open the existing class and wedge in another branch. The Strategy pattern eliminates that by letting you add new algorithms as new classes: without touching the code that uses them.

The Structure
The pattern has three participants:
Strategy interface: declares the method that all algorithms must implement.
Concrete strategies: each class implements the interface with a different algorithm.
Context: holds a reference to a strategy and delegates execution to it.
Payment Example
Consider a checkout system that supports multiple payment methods. Instead of hardcoding payment logic, you define a common interface and let each payment method implement it:
The Context Class
The context does not know or care which payment method is active. It holds a strategy reference and calls it:
The client code decides which strategy to inject:
The Checkout class never changes when you add a new payment method. You simply create a new class that implements PaymentStrategy and pass it in.
The Strategy pattern exists because of a specific, recurring problem: a method that branches on type to select an algorithm. Every time you see a function with if algorithm == "X" ... elif algorithm == "Y" ... elif algorithm == "Z", you are looking at a candidate for Strategy. Understanding the before-and-after transformation makes the pattern click.
The Problem: Conditional Algorithm Selection
Imagine a notification service that sends messages through different channels. The naive implementation puts all channel logic in one method:
This works for three channels. But what happens when you add Slack, WhatsApp, in-app notifications, and webhook callbacks? The send method balloons. Every addition forces you to modify this class, test the entire method, and risk breaking existing channels. The notification logic for every channel lives in one place: a violation of the Single Responsibility Principle.

The Solution: Extract Each Algorithm into Its Own Class
Apply the Strategy pattern. Define an interface, move each branch into a concrete class, and let the context delegate:
Now adding Slack is a new class, SlackNotification(NotificationStrategy), with zero changes to NotificationService or any existing notification class.
This is the Open-Closed Principle in action. The NotificationService is open for extension (new notification strategies) but closed for modification (existing code is never touched). In interviews, connecting Strategy to OCP demonstrates that you understand not just the pattern but the design principle it embodies.
What Encapsulation Buys You
Each strategy class encapsulates everything about one algorithm:
- Its own state, EmailNotification can hold SMTP credentials; SmsNotification can hold a Twilio API key. The context does not manage any of this.
- Its own tests, You write unit tests for EmailNotification in isolation. No need to set up SMS or push infrastructure to test email sending.
- Its own evolution, When the email team rewrites the HTML templating engine, they modify one class. The SMS and push teams are unaffected.
This isolation is what "encapsulate" means in the Gang of Four definition. Each algorithm is a self-contained unit that the rest of the system interacts with only through the strategy interface.
The Strategy pattern lets the client choose an algorithm. The State pattern solves a different problem: an object whose behavior changes based on its internal condition. The object does not just pick one algorithm and stick with it: it moves through a sequence of states during its lifetime, and the same method call produces different results depending on which state the object is in. To the caller, it looks like the object has changed its class.
The classic example is a vending machine. The same physical button does completely different things depending on whether the machine has a coin inserted, is currently dispensing, or is sold out. Rather than packing all those conditionals into one massive method, the State pattern delegates each behavior to a state object.

The State Interface
Every state in the vending machine implements the same interface, the set of actions a user can take:
Concrete States
Each state class implements these methods according to what makes sense in that state:
The Context: Vending Machine
The vending machine holds a reference to its current state and delegates all actions to it:
Notice how the VendingMachine's public methods are simple one-line delegations. All the behavioral logic lives in the state classes. When you call machine.insert_coin(), the response depends entirely on which state object is currently assigned: NoCoinState accepts the coin, HasCoinState rejects it, and SoldOutState returns it.
The real power of the State pattern is not just that behavior changes: it is that the transitions between states are explicit, traceable, and impossible to get wrong in the ways that conditional code gets wrong. Each state knows exactly which states it can transition to, and the transition happens through a clear method call rather than a reassignment of a string or enum buried in a 300-line function.
States Own Their Transitions
In the vending machine, NoCoinState.insert_coin() transitions to HasCoinState. HasCoinState.dispense() transitions to either NoCoinState or SoldOutState. Each state class declares its own exit conditions. You can read a single state class and know every possible transition out of that state without looking at any other code.
This is a dramatic improvement over the switch-statement approach:
Every method contains branches for every state. Adding a new state means modifying every method. Missing one branch creates a silent bug: the machine does nothing when it should reject an action, or worse, it dispenses an item without payment.

Transition Chains
States can trigger multi-step transitions. In the vending machine, pressing dispense in HasCoinState decrements the count and immediately transitions to either NoCoinState or SoldOutState. The caller does not orchestrate this sequence: the state handles it internally.
More complex systems have longer chains. A document approval workflow might move through Draft, Review, Approved, and Published. Each state's approve() method checks permissions and either transitions to the next state or rejects the action with a reason. The caller just calls document.approve() and the current state handles everything.
Watch out for state explosion. If you model every possible condition as a separate state class, you can end up with dozens of states that are hard to navigate. A vending machine with 4 states is clean. An e-commerce order with 15 states and 40 transitions needs a state machine diagram before you write code. If the number of states grows beyond what you can draw on a whiteboard, consider whether some states can be collapsed or whether a different pattern (like a state machine table) is more appropriate.
Testing State Transitions
Each state class can be tested independently. To test that HasCoinState.dispense() transitions correctly, you create a mock machine, set its item count, call dispense, and verify the state was set to the expected next state. You do not need to simulate an entire user session: just test each state's behavior and transitions in isolation.
This testability is a direct consequence of the pattern's structure. Each state is a unit with clear inputs (method call + context data) and clear outputs (behavior + state transition).
If you put the class diagrams for Strategy and State side by side, they look identical. Both define an interface with multiple implementations. Both use a context object that holds a reference to the current implementation and delegates method calls to it. This structural similarity is why interviewers love asking about the difference: it tests whether you understand the patterns at a behavioral level, not just a structural one.
The Critical Difference: Who Controls the Swap?
Strategy: the client chooses. The calling code selects which algorithm to use and injects it into the context. The context does not know why it has a particular strategy and does not change it on its own. A checkout system receives a CreditCardStrategy from the UI layer because the user selected credit card payment. The checkout processes it and is done.
State: the object changes itself. The context's behavior changes because its internal state transitions during its lifecycle. No external actor tells the vending machine to switch from HasCoinState to NoCoinState: the HasCoinState.dispense() method triggers that transition internally based on the machine's data.
This distinction drives every other difference:
| Aspect | Strategy | State |
| Who triggers the swap | Client (external) | State object (internal) |
| When swaps happen | Typically set once per operation | Transitions happen throughout the object lifecycle |
| Awareness of other implementations | Strategies do not know about each other | States know about and transition to other states |
| Context role | Passive, delegates and does not change strategy | Passive, delegates, but states change the context |
In interviews, the fastest way to distinguish Strategy from State is to ask: does the behavior change because an external caller made a choice, or because the object's internal condition changed? If a user picked an algorithm from a dropdown, that is Strategy. If the object moved from Pending to Shipped because a shipment was processed, that is State.
When to Use Which
Use Strategy when:
- The caller has the domain knowledge to select the algorithm (user picks a sort order, admin chooses an encryption method, configuration file specifies a logging format)
- The algorithm is set once and used for the duration of the operation
- The algorithms do not need to know about each other
Use State when:
- The object has a lifecycle with distinct phases (order processing, TCP connections, UI components with modes)
- Behavior must change in response to internal events, not external selection
- States need to trigger transitions to other states based on the object's data
Sometimes the boundary blurs. A text editor has a "mode" (insert, command, visual in Vim). Is that Strategy or State? It depends on implementation. If the user explicitly switches modes from the keyboard, and each mode simply handles keystrokes differently, it could be either. But if modes transition automatically (entering insert mode after pressing 'i' in command mode, returning to command mode after pressing Escape), that is State: the transitions are part of the object's behavior, not external choices.
Combining Both Patterns
Real systems often use both. A ride-sharing app might use the State pattern for trip lifecycle (Requested, DriverAssigned, InProgress, Completed) and the Strategy pattern for pricing algorithms (SurgePricing, FlatRatePricing, SubscriptionPricing). The State pattern manages what phase the trip is in. The Strategy pattern manages how the fare is calculated. Each pattern handles its own concern cleanly.
Loading problem...
Loading problem...
Loading problem...