Skip to main content
CleanCodeMastery

Strategy Pattern: Cycle, Bus, or Auto — You Choose

Learn the Strategy design pattern with a simple school travel story, easy TypeScript and C# code, runtime swapping, real examples, and practice tasks.

24 min read Updated June 11, 2026beginner
strategydesign patternsbehavioraltypescriptcompositionalgorithms

Ravi's three ways to reach school 🚲

Meet Ravi, a Class 7 student in Chennai. His school is 4 kilometres from home, and he has three ways to reach it:

  • On a nice cool morning, he rides his cycle. It is free, it is fun, and he gets exercise.
  • On a rainy day, Amma stops him at the door — "No cycle today!" — and he takes the bus. Cheap, dry, but he must walk to the bus stop.
  • On exam day, when his bag is heavy with books and he must reach early, Appa calls an auto. Costly, but fast and door to door.

Every morning the same small drama plays out at the door. Ravi looks at the sky. Amma looks at Ravi. Appa looks at the calendar. And one of three plans gets picked.

Now notice something important. Ravi's goal never changes: leave home, reach school by 8:30. What changes is the method of reaching there. Each method is a complete plan of its own — the cycle plan knows about the shortcut through the park, the bus plan knows the route number 47A, the auto plan knows how to haggle the fare.

And here is the key: Ravi decides each morning which plan to use. The plans do not decide for him. The cycle never says, "It is raining, switch to bus." Ravi looks out of the window and chooses.

Here is his morning decision, drawn as a flowchart — notice that the decision happens once, at the door, and after that the chosen plan simply runs:

Figure 1: The morning decision at the door — choose once, then follow the plan

This is the Strategy pattern in real life. One fixed goal, a family of interchangeable plans, and a chooser who picks one plan and follows it. The plans can be swapped any morning without changing Ravi himself. Keep this story in mind — Ravi, Amma, and Appa will stay with us for the whole post, and we will turn their morning into code soon.

One rainy Tuesday looks like this from Ravi's side:

Figure 2: A rainy Tuesday — the goal is fixed, only the plan was swapped

What is the Strategy pattern? 🧠

Strategy is a behavioral design pattern. It says: when a class can do one job in several different ways, do not stuff all those ways into the class with a big if-else. Instead, pull each way out into its own small class, make all of them follow one common interface, and let the main class hold a reference to one of them at a time.

The main class is called the Context (Ravi's trip). The common interface is the Strategy (a travel plan). The separate classes are Concrete Strategies (cycle plan, bus plan, auto plan). The context never knows which concrete plan it is holding. It only knows the interface — "every plan can take me to school" — and it delegates the work.

The Gang of Four book (1994) lists Strategy among the original 23 patterns and gives it a second name: Policy. The deep idea is one of the most quoted lines in software design: "Encapsulate what varies." The travelling varies, so we wrap each variation in its own object and make them swappable.

💡

One-line summary: Strategy = instead of deciding HOW to do something with if-else, the object decides WHO to ask, and you can change the "who" at any time.

Three roles to remember:

  • Context — holds a strategy and delegates work to it (Ravi's trip, or a shopping cart, or a router).
  • Strategy interface — the common promise every variant makes (go(distanceKm)).
  • Concrete strategies — the actual interchangeable algorithms (Cycle, Bus, Auto).

And here is a comparison of Ravi's three plans, side by side. Notice they are genuinely different algorithms for the same goal — different costs, different speeds, different trade-offs. That is exactly what makes them a strategy family:

PlanTime for 4 kmCostBest whenKnows about
Cycle~20 minRs 0Cool, dry morningPark shortcut
Bus 47A~26 minRs 10Rain, normal dayRoute and stops
Auto~12 min~Rs 90Exam day, heavy bagFare haggling

College corner: the philosophical move here is favour composition over inheritance — the second great principle from the GoF book. A beginner's instinct is to make CyclingStudent, BusStudent, and AutoStudent subclasses of Student. But then a student who cycles on Monday and buses on Tuesday needs to change class at runtime, which inheritance cannot do. Composition can: keep one Student, give him a replaceable plan field. Whenever you feel an urge to subclass just to change one behaviour, Strategy is usually the better answer.

The problem it solves 😵

Let us write Ravi's trip the painful way first — everything inside one class:

// BAD CODE: one method doing the job of three, glued with if-else
class SchoolTrip {
  travel(mode: string, distanceKm: number): void {
    if (mode === "cycle") {
      const minutes = distanceKm * 5;
      console.log(`Cycling via the park shortcut: ${minutes} min, Rs 0`);
    } else if (mode === "bus") {
      const minutes = 10 + distanceKm * 4; // walk to stop + ride
      console.log(`Bus route 47A: ${minutes} min, Rs 10`);
    } else if (mode === "auto") {
      const minutes = distanceKm * 3;
      const fare = 30 + distanceKm * 15;
      console.log(`Auto, door to door: ${minutes} min, Rs ${fare}`);
    } else {
      throw new Error("Unknown mode: " + mode);
    }
  }
}

This looks small now. Watch what happens as it grows:

  • Every new mode breaks open old code. Next month Ravi's friend Sandhya suggests the metro. You must edit this working, tested method and add another else if. The risk of breaking the bus logic while adding metro logic is real. This violates the Open/Closed Principle — code should be open for extension but closed for modification.
  • Unrelated logic is forced to live together. Auto fare haggling and cycle shortcuts share one method, one scope, one set of variables. A bug fix in one branch forces retesting of all branches.
  • The same mode === ... check leaks everywhere. The cost calculator checks the mode. The "what to pack" helper checks the mode. The mode string gets copied across the codebase, and one day someone types "buss" and nothing matches.
  • Testing is clumsy. To test only the auto fare formula, you must drive the whole travel method down one branch. You cannot pick up the auto algorithm alone.
Figure 3: Without the pattern, every part of the code repeats the same which-mode question

And the growth is not gentle. Every new travel mode adds a branch to every method that asks the which-mode question. With Strategy, every new mode is one new class and one new line in the chooser:

Figure 4: Conditional lines as travel modes grow — the ladder multiplies, the strategy family adds gently

The root disease: one class is doing the job of many. It owns the trip and every possible way of making the trip. Strategy keeps the trip and evicts the ways.

How it works, step by step 🛠️

The refactoring recipe:

  1. Find the algorithm that varies. Here, the body of travel() changes with the mode. That is what we extract.
  2. Declare the Strategy interface. One method that every variant can honestly promise: go(distanceKm). Keep the signature as small as possible.
  3. Move each branch into its own class. CycleStrategy, BusStrategy, AutoStrategy — each implements the interface and carries only its own math and its own settings.
  4. Give the context a strategy field. SchoolTrip now stores a TravelStrategy and offers setStrategy() so it can be changed at runtime.
  5. Replace the conditional with delegation. Where the if-else stood, write one line: this.strategy.go(distance).
  6. Let the client choose. Ravi (the client code) looks at the weather and installs the right strategy. The choice now lives in exactly one place.
  7. Optional: a default strategy. Start with CycleStrategy so the context works even before anyone configures it.
Figure 5: Structure of the Strategy pattern — the trip delegates to whichever plan is plugged in

Note the dotted arrow from Ravi. He is the only one who knows concrete plan names. The trip itself stays blind — it sees only the interface.

Real-life code example 💻

Here is Ravi's full week in TypeScript. Watch the strategies being swapped at runtime — same trip object, different behaviour:

// ---------- The Strategy interface ----------
interface TravelStrategy {
  readonly name: string;
  go(distanceKm: number): void;
}
 
// ---------- Concrete strategies ----------
// Each plan is complete and self-contained.
class CycleStrategy implements TravelStrategy {
  readonly name = "Cycle";
  go(distanceKm: number): void {
    const minutes = distanceKm * 5;
    console.log(`  Cycling via the park shortcut: ${minutes} min, Rs 0`);
  }
}
 
class BusStrategy implements TravelStrategy {
  readonly name = "Bus 47A";
  go(distanceKm: number): void {
    const minutes = 10 + distanceKm * 4; // walk to the stop + ride
    console.log(`  Taking bus 47A: ${minutes} min, Rs 10`);
  }
}
 
class AutoStrategy implements TravelStrategy {
  readonly name = "Auto";
  private baseFare = 30;
  private perKm = 15;
  go(distanceKm: number): void {
    const minutes = distanceKm * 3;
    const fare = this.baseFare + this.perKm * distanceKm;
    console.log(`  Auto, door to door: ${minutes} min, Rs ${fare}`);
  }
}
 
// ---------- The Context ----------
// SchoolTrip never branches on the mode. It only delegates.
class SchoolTrip {
  private strategy: TravelStrategy = new CycleStrategy(); // default
 
  setStrategy(s: TravelStrategy): void {
    console.log(`[trip] plan changed to: ${s.name}`);
    this.strategy = s;
  }
 
  start(distanceKm: number): void {
    this.strategy.go(distanceKm); // ONE line, no if-else
  }
}
 
// ---------- Ravi's week (the client) ----------
const trip = new SchoolTrip();
const DISTANCE = 4;
 
console.log("Monday, cool morning:");
trip.start(DISTANCE);                  // default cycle plan
 
console.log("Tuesday, heavy rain:");
trip.setStrategy(new BusStrategy());   // swap at runtime!
trip.start(DISTANCE);
 
console.log("Wednesday, exam day, heavy bag:");
trip.setStrategy(new AutoStrategy());  // swap again!
trip.start(DISTANCE);

The output:

Monday, cool morning:
  Cycling via the park shortcut: 20 min, Rs 0
Tuesday, heavy rain:
[trip] plan changed to: Bus 47A
  Taking bus 47A: 26 min, Rs 10
Wednesday, exam day, heavy bag:
[trip] plan changed to: Auto
  Auto, door to door: 12 min, Rs 90

Three important observations:

  1. The trip object was created once and behaved three different ways. The behaviour was swapped while the program ran, not at compile time. This runtime swap is Strategy's signature move.
  2. The strategies never touched each other. Bus does not know Auto exists. Only Ravi (client code) chooses. Compare this with the State pattern, where the fan's Slow state itself installs Medium.
  3. Adding the metro is purely additive. Write class MetroStrategy implements TravelStrategy, done. SchoolTrip, CycleStrategy, BusStrategy, AutoStrategy — none of them is opened. Old code stays safe.

Here is Tuesday morning traced call by call. Ravi talks only to the trip; the trip talks only to the interface; the bus plan answers:

Figure 6: Tuesday traced — Ravi installs the plan, the trip blindly delegates, the plan reports back

If you want the choice itself in one tidy place, pair Strategy with a tiny factory:

function planFor(weather: string, examDay: boolean): TravelStrategy {
  if (examDay) return new AutoStrategy();
  if (weather === "rain") return new BusStrategy();
  return new CycleStrategy();
}
 
trip.setStrategy(planFor("rain", false)); // the ONLY if-else left

The if-else did not vanish completely — it moved to one single decision point. That is the honest promise of Strategy: not "no conditionals ever", but "the which-one question is asked exactly once, in exactly one place".

The same idea in C# 💳

Ravi's elder sister Priya works part-time at a stationery shop. Her billing program has swappable discount rules — the same pattern, grown-up clothes. First the classic interface form, then the lambda shortcut that modern languages give us:

// Classic form: interface + classes
interface IDiscountStrategy
{
    decimal Apply(decimal subtotal);
}
 
class NoDiscount : IDiscountStrategy
{
    public decimal Apply(decimal subtotal) => subtotal;
}
 
class FestivalOffer : IDiscountStrategy   // 15% off for Diwali
{
    public decimal Apply(decimal subtotal) => subtotal * 0.85m;
}
 
class Bill
{
    private IDiscountStrategy _discount = new NoDiscount();
    public void SetDiscount(IDiscountStrategy d) => _discount = d;
    public decimal Total(decimal subtotal) => _discount.Apply(subtotal);
}
 
var bill = new Bill();
Console.WriteLine(bill.Total(1000));          // 1000
bill.SetDiscount(new FestivalOffer());
Console.WriteLine(bill.Total(1000));          // 850

Now the shortcut. When a strategy is just one method with little state, a function value does the same job with far less ceremony:

// Function form: the strategy is just a Func
class QuickBill
{
    private Func<decimal, decimal> _discount = s => s; // no discount
 
    public void SetDiscount(Func<decimal, decimal> d) => _discount = d;
    public decimal Total(decimal subtotal) => _discount(subtotal);
}
 
var qb = new QuickBill();
qb.SetDiscount(s => s * 0.85m);            // festival: 15% off, one lambda
Console.WriteLine(qb.Total(1000));         // 850
qb.SetDiscount(s => s >= 500 ? s - 50 : s); // flat Rs 50 off above Rs 500
Console.WriteLine(qb.Total(1000));         // 950

Both forms are the Strategy pattern. Use the class form when a strategy needs several methods (like Compress and Decompress), carries real configuration, or must be a named type registered in a factory. Use the lambda form when it is genuinely one small operation.

College corner: in functional programming terms, a one-method strategy is a higher-order function argument. Collections.sort(list, comparator) in Java, sorted(key=...) in Python, Array.prototype.sort(fn) in JavaScript — all of these are the Strategy pattern with the class ceremony stripped away. This is why some FP folks tease that "Strategy is just a function". They are right about the mechanism and wrong about the intent: the pattern's value is the named, swappable family with a stable contract, and that idea survives whether you spell it as classes or closures. Dependency injection containers push the same idea further — the strategy choice moves out of code entirely, into configuration.

And once more in Python 🐍

Python makes the lambda form almost invisible. Sorting Ravi's class marks three ways needs no interface at all — the key parameter is the strategy slot:

students = [
    ("Ravi", 82, 14), ("Sandhya", 95, 3), ("Imran", 88, 21),
]  # (name, marks, roll number)
 
# Three strategies for one job: ordering students
by_marks = sorted(students, key=lambda s: -s[1])  # toppers first
by_name  = sorted(students, key=lambda s: s[0])   # alphabetical
by_roll  = sorted(students, key=lambda s: s[2])   # roll number
 
print([s[0] for s in by_marks])  # ['Sandhya', 'Imran', 'Ravi']
print([s[0] for s in by_name])   # ['Imran', 'Ravi', 'Sandhya']
print([s[0] for s in by_roll])   # ['Sandhya', 'Ravi', 'Imran']

The sorting machinery is the fixed context. The key function is the plugged-in strategy. You have been using this pattern since your first week of Python — you just did not know its name.

Strategy is not a state machine 🚦

One picture to prevent next month's confusion. The trip itself does pass through stages — at home, on the way, at school. That is a tiny state machine of the journey:

Figure 7: The trip has lifecycle states, but the strategy never changes itself mid-ride — that is the difference from the State pattern

But look closely: while the trip moves through these states, the strategy stays the same. The bus plan does not morph into the auto plan halfway. In the State pattern, the variants themselves push the context from one to the next (Off installs Slow, Slow installs Medium). In Strategy, the variants are strangers, and only the client swaps them — at most once per morning, at the door. If you ever catch your "strategies" calling context.setStrategy(...) on each other, stop: you have accidentally built a state machine and should design it as State.

Where you see it in real software 🌍

Strategy may be the most-used pattern in everyday programming:

  • Sorting comparators. Java's Collections.sort(list, comparator) is the textbook real example — the sort algorithm is fixed, but the comparison strategy is plugged in by you. JavaScript's array.sort((a, b) => ...) and C#'s IComparer<T> are the same idea. Every time you pass a comparator, you are handing over a strategy.
  • Payment selection at checkout. Indian apps live this pattern daily: pay by UPI, card, net banking, wallet, or cash on delivery. Each payment processor (Razorpay, Stripe, PayPal adapters) sits behind one common interface, and the chosen one is installed at checkout time.
  • Compression and encoding choices. Zip vs gzip vs brotli; the caller picks a codec, the pipeline stays the same. Web servers negotiate the compression strategy per request.
  • Authentication strategies. The Node.js library Passport.js literally calls its plugins "strategies" — login with Google, with Facebook, with username/password. Hundreds of interchangeable strategies behind one interface.
  • Route planning in maps. Car, bike, walk, public transport — one button swap changes the whole routing algorithm, while the app's UI never changes. Ravi's morning choice, at planet scale.
  • Validation and pricing rules in business apps, image filters in editors, retry policies in HTTP clients (fixed delay vs exponential backoff) — all strategy menus.
  • Open-source code to read. The iluwatar/java-design-patterns strategy folder shows both the class form and the lambda form side by side, and refactoring.guru's Strategy page has examples in ten languages.

And to close the loop on our story — if you surveyed Ravi's whole school about their daily travel strategy, the menu would look something like this:

Figure 8: One goal, many strategies — how students in Ravi's school reach it

Every slice is a complete, self-contained plan. No slice knows the others exist. The chooser is the student at the door. That pie is the pattern.

When to use it and when not to 🤔

SituationUse Strategy?
One method has a fat if-else choosing between different ways of doing the same job✅ Yes — extract each way
New variants keep arriving (new payment modes, new shipping rules)✅ Yes — adding becomes purely additive
You must switch behaviour at runtime (user choice, config, weather!)✅ Yes — swap the object
You want to test each algorithm alone✅ Yes — each strategy tests in isolation
Many subclasses differ only in one behaviour✅ Yes — one context + strategies beats a class explosion
Only one or two stable behaviours that never change❌ No — plain code is clearer
The variants must switch each other automatically (lifecycle stages)❌ No — that is the State pattern
The whole "strategy" is one tiny expression❌ Use a plain function/lambda instead of classes
The steps are fixed but one middle step varies in subclasses❌ Consider Template Method

When you are torn between Strategy, State, and plain code, place your problem on this map. The two questions that matter: who triggers the change of behaviour, and does the behaviour change at runtime at all?

Figure 9: Find your problem on the map before reaching for a pattern

Ravi's choice sits firmly in Strategy land: the behaviour changes daily (runtime), and the caller (Ravi at the door) picks it. The ceiling fan from the State post sits opposite: the object itself walks through its own stages.

Common mistakes students make 🚧

⚠️

The number one mistake: moving the if-else inside the context's method instead of removing it. Some students create the strategy classes but then write if (strategy instanceof BusStrategy) inside the context. The context must stay blind. It holds an interface, calls one method, and never peeks at the concrete type. The only place allowed to know concrete names is the client or a small factory.

More traps:

  1. A bloated strategy interface. If your interface has six methods and most strategies leave four empty, the interface is too fat. Keep it to the operations every variant truly shares.
  2. Strategies that secretly depend on the context's internals. A strategy should receive what it needs as parameters (or constructor settings). If it reaches into the context's private fields, the "independent, swappable" promise is broken.
  3. Forgetting the default. A context whose strategy is null crashes on first use. Give it a sensible default or a do-nothing strategy (like NoDiscount) so it always works.
  4. Using classes where a lambda is enough. Twenty lines of ceremony for s => s * 0.85 is over-engineering. The pattern is the idea of swappable behaviour, not the keyword class.
  5. Confusing Strategy with State. If you find your strategies calling context.setStrategy(...) on each other, stop — you have built a state machine and should name and design it as State.
💡

Interview tip: when asked "give a real example of Strategy you used this week", say sorting with a comparator. You almost certainly did it, and it shows you understand that passing behaviour as a value is the pattern's heart.

Compare with cousins 👯

PatternHow behaviour variesMechanismWhen chosenWho triggers the change
StrategyWhole algorithm is swappedComposition (context holds an object)At runtimeThe client
StateBehaviour changes with internal stageComposition (identical skeleton!)At runtimeThe states themselves
Template MethodOnly certain steps varyInheritance (subclass overrides steps)At compile timeFixed by the subclass chosen
CommandAn action is wrapped to run later/undoCompositionAt runtimeThe invoker queues it
DecoratorExtra behaviour is layered onComposition (wrapping)At runtimeWrapping order

Two comparisons matter most for exams:

Strategy vs State — the famous twins. The class diagram is identical: context, interface, concrete classes. The difference is intent. Strategy variants are a flat menu: independent, unaware of each other, picked from outside (cash/UPI/card; cycle/bus/auto). State variants are a connected graph: each stage knows its next stage and pushes the context along (Off → Slow → Medium).

College corner: the blunt one-line test for the twins: if a variant ever calls context.setState(otherVariant), it is State; if only the client swaps, it is Strategy. A second framing that interviewers love: Strategy answers "how should this job be done?" while State answers "what can this object do right now?" Strategy choices are usually all valid simultaneously (any payment mode works today); State stages are usually mutually exclusive points on a timeline (an order cannot be both Placed and Delivered). If you can draw arrows between the variants, you are looking at State. If the variants form a menu card, you are looking at Strategy.

Strategy vs Template Method — the same problem, opposite tools. Both let part of a behaviour vary. Template Method uses inheritance: a base class fixes the skeleton and the subclass fills in steps, decided at compile time. Strategy uses composition: the context holds a separate object and can replace the whole algorithm at runtime. If you need runtime swapping or want to avoid single-inheritance chains, prefer Strategy; if many variants share a fixed sequence of steps, Template Method shines.

Figure 10: Strategy and its cousins — same goal of varying behaviour, different mechanisms

The whole pattern on one page 🗺️

If you can redraw this mind map from memory, you own the pattern:

Figure 11: The Strategy pattern, mapped — roles, key move, shortcuts, and where it lives

Quick revision box 📦

+--------------------------------------------------------------+
|                  STRATEGY PATTERN — REVISION                 |
+--------------------------------------------------------------+
| WHAT     : Family of interchangeable algorithms, each in     |
|            its own class behind one interface                |
| ACTORS   : Context + Strategy interface + concrete           |
|            strategies + Client who picks                     |
| KEY MOVE : Context delegates; client swaps at runtime        |
| KILLS    : Fat if-else choosing between ways of one job      |
| REMEMBER : Strategies never know each other (else: State)    |
| SHORTCUT : One-method strategy ~= a plain function/lambda    |
| AVOID IF : 1-2 stable variants; ceremony beats benefit       |
| EXAMPLES : Comparators, payment modes, compression codecs,   |
|            Passport.js auth, map routing                     |
+--------------------------------------------------------------+

Practice exercise ✍️

Write real code for these. Typing it yourself is half the learning.

  1. Shop payment counter. Build a Checkout context with strategies CashPayment, UpiPayment, and CardPayment. Each prints a different flow (counting change, scanning QR, swiping and PIN). Bill three customers with three different methods using the same checkout object, swapping the strategy between customers.
  2. Marks sorter. You have an array of students with name, marks, and roll number. Using only your language's built-in sort with a comparator, sort it three ways: by marks descending, by name alphabetically, by roll number. Then write one sentence: why is the comparator a strategy?
  3. Upgrade challenge. Take Ravi's SchoolTrip and add (a) a MetroStrategy, and (b) the small planFor(weather, examDay) factory shown above, extended with a "metro on strike days" rule. Confirm that adding metro touched zero existing classes — if it did touch one, refactor until it does not.
  4. College stretch. Rewrite the whole travel example with no classes at all: strategies as plain functions in a map keyed by mode name, the context holding one function reference. Then write three sentences comparing the two versions: which is shorter, which is easier to discover in a big codebase, and which would you pick for a team of forty developers?

Frequently asked questions

What is the Strategy pattern in one line?
Take each algorithm out of the main class, put each one in its own small class behind a common interface, and let the main class delegate to whichever one is plugged in. Swapping the plug swaps the behaviour.
How is Strategy different from State?
Their class diagrams look identical, but intent differs. In Strategy, the client picks one algorithm and the algorithms do not know each other. In State, the state objects know each other and switch the context themselves, like stages of a machine.
Can a simple function replace a strategy class?
Yes, very often. In languages with first-class functions, a one-method strategy is just a function or lambda. Use classes when a strategy needs several methods, carries its own settings, or must be a named, discoverable type.
Does Strategy remove all if-else from my program?
No, it moves the choice to one single place. Somewhere, the client or a factory still decides which strategy to create. The win is that this decision now lives in exactly one spot instead of being copied across many methods.
When is Strategy unnecessary?
If you have only one or two simple behaviours that almost never change, the extra interface and classes are just ceremony. Reach for the pattern when variants keep arriving or when one method has grown a fat conditional choosing between behaviours.

Further reading

Related Lessons