Design Patterns

Behavioural Design Patterns

The first of four lessons on behavioural patterns: Strategy, Template Method and State, the three ways to vary behaviour. Strategy swaps an algorithm held in a field, and must not share mutable state between contexts; Template Method fixes the order of steps in a base class, and only protects the steps it makes private or final; State moves state-dependent behaviour into one class per state, so the compiler makes every new state answer every operation. When to use each, how State differs from Strategy, and every example running in Java and Python with verified output.

Suggest an edit

Behavioural Design Patterns — Strategy, Template Method and State

Behavioural patterns are about how objects interact and divide up the work: who decides what, who calls whom, and how behaviour changes over time. They simplify complicated communication between objects while keeping them loosely coupled. Think of a TV remote that lets you step through channels one by one without knowing how the channels are stored: that kind of controlled interaction is what behavioural patterns help you design.

This chapter covers the eleven GoF behavioural patterns in four lessons. This first lesson covers the three patterns that answer one question, how do I vary behaviour?: Strategy (swap the whole algorithm), Template Method (fix the algorithm's outline and vary some steps) and State (let behaviour change with the object's state). The next three lessons cover requests and undo (Command, Chain of Responsibility, Memento), object communication (Observer, Mediator) and traversal (Iterator, Visitor).

💡 The core idea.

  • Strategy puts each variant of an algorithm in its own class behind one interface; the object that uses it holds one in a field and can swap it at run time. It is composition.
  • Template Method puts the fixed outline of an algorithm in a base-class method and leaves some steps to subclasses. It is inheritance.
  • State gives each state of an object its own class; the object delegates to its current state, and the states decide the transitions.

This builds on Structural Design Patterns and on the abstract classes and template method from Abstraction & Interfaces. Every output below was produced by running the code on Java 21 and Python 3.11.

You'll be able to: replace a conditional that picks an algorithm with interchangeable strategies, and keep strategies safe to share; write a template method whose fixed steps really can't be changed by subclasses; model an object's lifecycle with the State pattern, and say what adding a new state costs; tell State from Strategy by who chooses the behaviour and whether the variants know about each other.

📘 How to read the Intuition boxes. Each one is built in three moves:

  1. The mechanism — where the varying behaviour lives, and who decides which variant runs.
  2. A concrete bite — a specific, runnable program where that decision is made in the wrong place.
  3. The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.

Table of contents

  1. Strategy pattern
  2. Template Method pattern
  3. State pattern
  4. State or Strategy?
  5. Mental-model summary
  6. Gotcha checklist
  7. Check yourself
  8. Sources

1. Strategy pattern

A navigation app can plan a route for driving, walking or cycling, and the algorithm depends on the mode chosen. Instead of hard-coding every algorithm inside one class, each one can be defined separately and chosen while the app runs. That is what the Strategy pattern does: it lets a class choose its behaviour at run time by putting related algorithms into interchangeable objects.

📘 Definition. "Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it" [1].

Each algorithm (each strategy) lives in its own class, behind a common interface, and the object that uses it (the context) can switch between them without changing its own class. That keeps related algorithms organised, and makes the code flexible and easy to extend.

💡 Insight. A ride-hailing app matches riders with drivers, and the matching algorithm depends on the situation: the nearest driver, priority for surge zones, or first in line at an airport queue.

In this example, the ride-matching service is the context; the matching algorithms (nearest, surge priority, airport queue) are the strategies; and the strategy interface lets the service switch between them as conditions change, without its own code changing.

Understanding the problem

Here is a ride-matching service with every algorithm in one method:

Output:

Matching rider at Downtown with nearest driver.
Matching rider at City Center based on surge pricing priority.
Matching rider at Airport Terminal 1 from airport queue.

Problems with this approach.

Issue Explanation
Violates the Open/Closed Principle Adding a strategy, such as VIP matching, means modifying RideMatchingService, so the strategies and the core class are tightly coupled.
The code gets messy Every new condition adds a branch, and the method gets harder to read and maintain.
Hard to test or reuse No single matching algorithm can be tested or reused on its own; they are all inside one method.
No separation of concerns The class both coordinates (the service) and implements (the algorithms), which makes it less flexible.

The solution

The Strategy pattern removes the conditional by giving each matching algorithm its own class. The service delegates to whichever strategy it currently holds:

Output:

Matching using FIFO airport queue for Terminal 1
Matching with the nearest available driver to Downtown
Matching rider using surge pricing priority near Downtown

In Python, functions are objects, so a plain function or lambda is often enough as a strategy: RideMatchingService(lambda loc: print(...)). The class-based form above mirrors the Java structure one-to-one, which helps when a strategy needs configuration or several methods.

How this solves the earlier problems.

Problem in the old approach How the Strategy pattern solves it
Violates the Open/Closed Principle A new strategy is a new class implementing MatchingStrategy; the service doesn't change.
The code gets messy The if/else chain is gone; each behaviour is its own class.
Hard to test or reuse Each strategy can be tested on its own, and reused by other services.
No separation of concerns RideMatchingService only coordinates; the algorithms live in the strategy classes.

Analysis. The downtown service switched from nearest-driver to surge matching with one setStrategy call, and its own code never mentions either algorithm. Who picks the strategy is decided outside the context: by the code that creates the service, or by whatever detects that surge has started. The strategies don't know about each other.

Intuition. Mechanism. The context holds a reference to a strategy object and forwards the work to it. The strategy is chosen from outside, and can be replaced at any time. Because a strategy is an object, it can also hold data, and that data travels with the object, not with the context that happens to use it.

Concrete bite. An airport queue strategy really does hold data: the queue of drivers waiting at that airport. Here one strategy instance is shared by two terminals' services, because "it's the same algorithm":

Output:

Terminal 1 rider -> T1-driver-A
Terminal 2 rider -> T1-driver-B
Terminal 1 rider -> null

Terminal 2's rider was given a driver waiting at Terminal 1, and the next Terminal 1 rider found the queue empty. The algorithm was the same for both terminals, but the data wasn't, and sharing the object shared the data.

💡 Earned rule. Replace a conditional that picks an algorithm with strategies when the algorithms keep growing or need testing on their own. Keep strategies stateless where you can, so one instance can be shared freely; when a strategy must hold state (a queue, a counter, a cache), give each context its own instance.

The cost is a class per algorithm, and a client that must know which strategies exist. For two fixed variants that never change, an if is simpler.

Suitable scenarios for the Strategy pattern.

  • Several interchangeable algorithms: a system supports different behaviours that are swapped by context or configuration.
  • The Open/Closed Principle: new behaviours must be added without modifying existing business logic.
  • Removing conditionals: large if/else or switch blocks that select a behaviour can be split into classes.
  • Testing behaviours on their own: each strategy can be tested in isolation from the context.
  • Choosing behaviour at run time: from user input, configuration or the environment.

Pros of the Strategy pattern.

  • Open/Closed Principle: new strategies don't require changes to existing code.
  • Easy to add behaviours: each behaviour is self-contained.
  • Run-time changes: behaviour can be swapped while the program runs.
  • Composition over inheritance: behaviour is plugged in, not inherited, which keeps the design flexible.

Cons of the Strategy pattern.

  • Many small classes: one class per strategy adds code.
  • Clients must know the strategies: the code that picks one must know which exist and when to use each.
  • Overhead of interfaces: extra structure that simple logic may not need.
  • More complex than if/else in very simple cases.

2. Template Method pattern

Baking a cake from a recipe, the steps are fixed: mix the ingredients, preheat the oven, bake, cool. The details, such as the ingredients or the flavour, vary from cake to cake. The Template Method pattern works the same way: a base class defines the fixed structure of an algorithm, and subclasses fill in some of its steps.

📘 Definition. "Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure" [1].

The invariant parts of the algorithm stay in one place and can't be changed, while the variable parts can be customised. The recipe is the template: it fixes the steps, and each kind of cake supplies its own ingredients.

The four parts of a template method.

  • The template method: a method in the base class, usually final, that defines the skeleton of the algorithm by calling the steps in order. Making it final stops subclasses changing the structure.
  • Primitive operations: abstract methods that subclasses must implement. They are the variable parts of the algorithm.
  • Concrete operations: methods with behaviour common to every subclass, defined once in the base class. Making them private or final stops subclasses changing them.
  • Hooks: optional methods with default behaviour in the base class. Subclasses may override them, but don't have to.

Understanding the problem

A notification service sends messages by email and by SMS. Each class does the whole job itself:

Output:

Checking rate limits for: [email protected]
Validating email recipient: [email protected]
Logging before send: Your order has been placed! to [email protected]
Sending EMAIL to [email protected] with content:
<html><body><p>Your order has been placed!</p></body></html>
Analytics updated for: [email protected]
 
Checking rate limits for: 1234567890
Validating phone number: 1234567890
Logging before send: Your OTP is 1234. to 1234567890
Sending SMS to 1234567890 with message: [SMS] Your OTP is 1234.
Custom SMS analytics for: 1234567890

Issues in this code.

  • Duplication: both classes contain nearly identical code for rate limiting, formatting, logging and analytics, which breaks DRY and makes the code harder to maintain.
  • Hard-coded behaviour: the whole procedure is inside each send(), so a new channel, such as push notifications, means copying all of it again.
  • Not extensible: changing the rate-limit, logging or analytics logic means editing every channel class, with a risk of missing one.
  • Maintenance overhead: every new channel adds another class full of the same code.

The solution

The Template Method pattern moves the shared procedure into a base class. Its send() is the template method: it runs the common steps, and calls abstract methods for the steps that differ by channel:

Output:

Checking rate limits for: [email protected]
Validating recipient: [email protected]
Logging before send: Welcome to the platform! to [email protected]
Sending EMAIL to [email protected] with content:
<html><body><p>Welcome to the platform!</p></body></html>
Analytics updated for: [email protected]
 
Checking rate limits for: 9876543210
Validating recipient: 9876543210
Logging before send: Your OTP is 4567. to 9876543210
Sending SMS to 9876543210 with message: [SMS] Your OTP is 4567.
Custom SMS analytics for: 9876543210

The four parts in this code.

  • Template method: send() defines the skeleton. It calls the common steps (rateLimitCheck, validateRecipient, formatMessage, preSendAuditLog) and delegates the channel-specific ones to subclasses.
  • Primitive operations: composeMessage() and sendMessage() are abstract, so EmailNotification and SMSNotification must implement them.
  • Concrete operations: rateLimitCheck, validateRecipient, formatMessage and preSendAuditLog hold the logic shared by every channel, and are private, so no subclass can change them.
  • Hook: postSendAnalytics has a default, which SMSNotification overrides with custom analytics and EmailNotification keeps.

How this solves the issues.

Issue Solution with Template Method
Duplication The common steps (rate limiting, validation, logging) live once, in the base class.
Hard-coded behaviour Channel-specific behaviour is in subclasses, which keeps the code flexible.
Not extensible A new channel, such as PushNotification, extends NotificationSender and implements two methods.
Maintenance overhead Changing rate limiting or logging means editing one place, the base class.

Analysis. Both channels ran the same six steps in the same order, but neither subclass contains a single line of the shared logic. In the diagram, the private (-) operations are the fixed part, the italic abstract operations (*) are the parts each subclass must supply, and the protected (#) postSendAnalytics is the hook. send() is final in the Java code; UML would show that as the property {leaf}, which Mermaid can't draw.

Intuition. Mechanism. The template method calls its steps through this, so each call goes to the subclass's version if there is one. That is how the variable steps get customised, and it is also how the fixed steps get customised, by accident or on purpose, unless the base class prevents it. Only private and final (in Python, name mangling, plus a static @final check) keep a step fixed.

Concrete bite. Here the rate-limit step was left protected and overridable, and someone wrote an OTP sender that "must never be delayed":

Output:

SMSNotification:
SMS to 98765: OTP 1
SMS to 98765: OTP 2
Rate limit reached for 98765; not sent
FastOtpNotification:
SMS to 98765: OTP 1
SMS to 98765: OTP 2
SMS to 98765: OTP 3

send() was final, so its structure looked protected, yet the rate limit, the step that protects users from message floods, was switched off by a three-line subclass. A template is only as fixed as the steps it calls: here the skeleton was locked, but one of its bones was replaceable.

💡 Earned rule. Use Template Method when several classes follow the same procedure and differ in a few steps. Make the template method final, make every step that must not vary private (or final), make the varying steps abstract, and offer hooks only where variation is genuinely allowed. In Python, mark the template @final and give the fixed steps double-underscore names.

The cost is inheritance: each variant is a subclass, tied to the base class's structure, and a class can only have one base. When the varying part is the whole algorithm, or must be swapped at run time, use Strategy instead.

When to use the Template Method pattern.

  • Several classes follow the same algorithm, but differ in a few steps.
  • You want to avoid duplicating the common steps.
  • The steps must run in a fixed order.
  • You want to allow optional customisations, through hooks.
  • You need a structured flow that subclasses follow, while filling in specific details.

Pros of the Template Method pattern.

  • Reuse: common steps are shared, and every subclass follows the same algorithm without duplicating code.
  • Open/Closed Principle: new variants are new subclasses, with no change to the base class.
  • A consistent flow: the order of the steps is fixed and predictable for every subclass.
  • Optional customisation: hooks let subclasses adjust behaviour without changing the structure.

Cons of the Template Method pattern.

  • Based on inheritance: behaviour is tied to the base class, which limits flexibility.
  • Tight coupling: a change to the base class can affect every subclass.
  • A poor fit when the whole algorithm varies: then Strategy is the better choice.
  • Many subclasses: if many steps vary independently, the number of subclasses grows.

Real-world uses of Template Method.

  1. Payment flows. A payment platform runs domestic and international payments through the same sequence (validate the payment method, process the payment, update the account), while the details, such as validating a UPI ID or a card, differ by subclass.
  2. Game engines. Engines such as Unity and Unreal fix the shape of the game loop (handle input, update physics, render), and let each game customise steps such as AI decisions or rendering.
  3. Frameworks and libraries. Java's HttpServlet.service() is a template method: it dispatches each request to doGet, doPost and so on, which subclasses override. java.util.AbstractList implements most list operations in terms of two primitive operations, get and size, that subclasses supply. Django's class-based views dispatch to get() and post() in the same way.

3. State pattern

A vending machine behaves differently depending on what has happened: when you have inserted enough money it gives you a snack, and when you haven't, it asks for more. Behaviour that changes with an object's state is what the State pattern organises.

📘 Definition. "Allow an object to alter its behavior when its internal state changes. The object will appear to change its class" [1].

The State pattern puts the behaviour for each state into its own class, and the object (the context) delegates each operation to its current state object. Each state class is independent of the others' internals, and the transitions between states are explicit.

Real-life analogy. In a food delivery app, an order moves through several stages: placed, being prepared, a delivery partner assigned, picked up, out for delivery, and finally delivered. At each stage the app behaves differently: when the order has just been placed you can cancel it; while it is being prepared you can track its progress; once a partner is assigned you can see their details; and so on until delivery.

💡 Insight. Each stage is a distinct state, and the app's behaviour depends on which state the order is in. With the State pattern, each state class controls the behaviour for its stage, and decides which state comes next.

Understanding the problem

Here is an order whose state is a string, with every operation switching on it:

Output:

Initial State: ORDER_PLACED
Order moved to: PREPARING
Order moved to: OUT_FOR_DELIVERY
Order moved to: DELIVERED
Cannot cancel the order now.
Final State: DELIVERED

The code works, but it has serious problems.

Issues in the code.

  • Transitions are hard-coded: they live in a switch in nextState(), which gets unwieldy as states are added.
  • Poor encapsulation: the transition logic and the cancellation rules are all inside Order, which mixes several responsibilities in one class.
  • Duplication: every new operation repeats a check of the state string, and every new state must be added to every such check.
  • Fragile to change: adding a state or changing a behaviour means editing Order each time, which is error-prone.

The solution

With the State pattern, each state is a class that implements every operation for that state, and decides the next state:

Output:

Current State: ORDER_PLACED
Order is now being prepared.
Order is out for delivery.
Cannot cancel. Order is out for delivery.
Order has been delivered.
Cannot cancel a delivered order.
Final State: DELIVERED

How this solves the issues.

Issue Solution with the State pattern
Transitions are hard-coded Each state class decides its own transitions, so OrderContext doesn't change when they do.
Poor encapsulation Each state's behaviour is in its own class, which keeps responsibilities separate.
Duplication There are no repeated checks of a state string; each class simply implements its own behaviour.
Fragile to change A new state is a new class implementing OrderState; only the states that transition into it need to change.

Analysis. OrderContext contains no if and no switch: each call goes to the current state object, which does the right thing and, when appropriate, replaces itself with the next state. The rules for each stage are in one place, its class. The class diagram shows the shape: one context holding one current state, and one class per state, all implementing the same interface.

Intuition. Mechanism. In the string version, a state is a value that every method checks; in the State pattern, a state is an object that every method is sent to. The interface lists every operation, so every state class must say what it does for each one. Adding a state means writing one class, and editing only the states that can transition into it.

Concrete bite. The business adds a new stage, "partner assigned", between preparing and out-for-delivery, and the rule is that customers may cancel until the order is picked up. In the string version, a developer adds the new state to nextState(), the method they were looking at:

Output:

Order moved to: PREPARING
Order moved to: PARTNER_ASSIGNED
Cannot cancel the order now.

The customer was refused a cancellation the business allows. Nothing failed: the new state silently fell into the else branch of a method nobody remembered to update, and in a real codebase there would be many such methods. With the State pattern, PartnerAssignedState can't compile until it implements cancel(), so the developer is forced to decide what cancelling means in the new state, and the only existing class to edit is PreparingState, whose next() must now lead to it.

💡 Earned rule. When an object has several states and more than one or two operations whose behaviour depends on the state, give each state its own class behind an interface that lists every operation. Then the compiler checks that every state answers every operation.

The cost is a class per state, and transitions spread across those classes, so the full state diagram is no longer visible in one place; draw it, or keep a test that walks every transition. For two states and one operation, a boolean and an if are simpler.

When to use the State pattern.

  • Behaviour depends on internal state: an object must act differently depending on its condition or phase.
  • Well-defined, finite transitions: the states and the moves between them are clear and limited in number.
  • Avoiding large conditionals: you want to remove bulky if/else or switch blocks that check the current state.
  • Explicit transitions: it matters that moves between states are clear and maintainable.
  • Distinct behaviour per state: each state has its own rules, which are better kept in separate classes.

Pros of the State pattern.

  • Clear separation of state behaviour: each state's behaviour is in its own class, easy to manage and change without affecting the rest.
  • Easy to add states: a new state is a new class implementing the interface, plus edits to the states that lead to it.
  • Open/Closed Principle (mostly): existing states that don't transition to the new one are untouched.
  • No large conditionals: each state class handles its own logic, which improves readability.

Cons of the State pattern.

  • More classes: each state needs its own class, which adds to the size of the design.
  • More set-up: defining the interface and several state classes takes more initial work than a simple switch.
  • Transitions must be managed carefully: the context and the states share the job of moving between states, which needs care to keep consistent.
  • Requires familiarity: developers who don't know the pattern have a learning curve.

Real-life examples.

  1. Food delivery orders. An order goes through placed, preparing, out for delivery, delivered or cancelled. Each state allows different actions: a newly placed order can be cancelled; one out for delivery can't, but offers live tracking. Each state can be a class implementing OrderState.
  2. Ride-hailing trips. A ride moves through requested, driver assigned, accepted, in progress, completed or cancelled. While a ride is requested the rider can cancel; once it is in progress, cancelling is replaced by real-time tracking.
  3. ATMs. An ATM moves between idle, card inserted, PIN entered, transaction in progress, transaction completed and out of service. Each state allows only the actions that make sense, so you can't, for example, enter a PIN after the transaction has finished.

4. State or Strategy?

State and Strategy have the same class diagram: a context holds a reference to an interface, and several classes implement it. They are easy to confuse, but they answer different questions.

Aspect State pattern Strategy pattern
Intent Change behaviour as the object's internal state changes Choose an algorithm for a task, from outside, at run time
Who switches The state objects themselves, as part of their behaviour (context.setState(...)) The client or configuration (setStrategy(...))
Do the variants know each other? Yes: a state knows which states can follow it No: strategies are independent and unaware of each other
What differs between variants What the object is allowed to do, and what happens next How a task is done; different strategies often produce the same kind of result in different ways
Typical uses Lifecycles, workflows, state machines Algorithm selection, pricing rules, formatting, matching

A quick test: if the variant changes by itself as a result of calls on the object (an order moves from "preparing" to "out for delivery"), it is State. If someone outside picks the variant and it stays until they pick another (the app switches to surge matching), it is Strategy.


5. Mental-model summary

Principle Consequence
Strategy: the algorithm is an object in a field Swap it at run time; new algorithms are new classes
Strategies can hold state Share stateless strategies; give each context its own stateful one
Template Method: the outline in a final base-class method Subclasses fill in abstract steps and optional hooks
A step is fixed only if it is private or final Overridable "fixed" steps can be switched off by any subclass
State: one class per state, the context delegates Every state must answer every operation; states decide transitions
A new state touches only the states that lead to it No hunting through every switch for a missing case
State vs Strategy Who switches (the states vs the client), and whether variants know each other

6. Gotcha checklist

Symptom Likely cause Fix
One context sees another context's data a stateful strategy instance is shared stateless strategies, or one instance per context
A one-method strategy class in Python Java structure copied without need pass a function
A subclass skips a step that should always run the step is overridable in the base class make it private/final; in Python, mangle the name and use @final
The order of steps differs between subclasses the template method isn't final make the template method final
A new state behaves wrongly in some operations state stored as a string or enum and checked in many places one class per state behind an interface
States can't be followed in the code transitions spread over many classes draw the state diagram; test every transition
Unsure whether it's State or Strategy same class diagram who switches, and do the variants know each other?

✅ Check yourself

One check per objective. Answer before you open anything.

A document workflow has Draft, In Review, Approved and Published states. Publishing always runs the same steps (validate, render, upload, notify), but rendering differs for PDF and HTML, and the upload target (S3 or a CMS) is chosen per customer in configuration. Which of the three patterns would you use where?

State for the workflow: DraftState, InReviewState, ApprovedState and PublishedState each implement operations such as submit(), approve(), publish() and edit(), and decide the transitions (only ApprovedState.publish() actually publishes). Template Method for publishing: a Publisher base class with a final publish() that runs validate, render and notify in order, with render() abstract (PdfPublisher, HtmlPublisher) and validation and notification private. Strategy for the upload target: an UploadTarget interface (S3Target, CmsTarget) injected into the publisher from configuration, because it is chosen from outside and independently of the format. If the upload targets hold connections, give each publisher its own instance.


📚 Sources

  1. Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994), ch. 5 "Behavioral Patterns": State, Strategy, Template Method.
  2. Joshua Bloch, Effective Java, 3rd edition (Addison-Wesley, 2018), Item 19 "Design and document for inheritance or else prohibit it".
  3. Python 3 documentation, typing.final and private name mangling — https://docs.python.org/3/library/typing.html#typing.final

🧪 Predict, then check.

  1. In the Strategy bite in section 1, give terminal2 its own AirportQueueStrategy with the driver "T2-driver-A". Predict the three lines.
  2. In the Java Template Method solution in section 2, add @Override protected void rateLimitCheck(String to) {} to SMSNotification. Predict what the compiler says. Then delete @Override and predict whether "Checking rate limits" still prints for the SMS.
  3. In the Python Template Method solution, define def _NotificationSender__rate_limit_check(self, to): pass in SMSNotification. Predict whether the rate-limit line still prints, and why.
  4. In the State solution in section 3, call cancel() straight after creating the order, then next(). Predict both lines.

Your Turn

Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.

Mark as readBook versionff97b7f