Almost every codebase eventually grows one: a method that starts with a simple if statement and, over the months, collects a dozen branches. Ship by ground, by air, by freight, by international courier. Each new requirement adds another else if, and each change risks breaking the branches that already work.

This is one of the most recognisable smells in object-oriented software. The method violates the Open/Closed Principle, because supporting a new behaviour means editing code that should be stable. It packs unrelated logic into one place, multiplies the number of tests you need, and makes every deployment a small gamble.

The Strategy pattern, catalogued by the Gang of Four in 1994, offers a disciplined alternative. Define a family of algorithms, put each one behind a common interface, and make them interchangeable. The client stops caring how the work is done and depends only on the fact that it can be done. What follows looks at when the pattern earns its place, what it buys you, and how the right strategy reaches the code that needs it.

Recognising an Algorithm Family

The first skill is recognition. You have a strategy candidate when several algorithms accomplish the same goal by different means. Sorting a collection with quicksort or merge sort. Compressing a file with gzip, zstd, or brotli. Pricing an order with standard, seasonal, or loyalty discounts. The inputs and outputs are shared. Only the internal approach differs.

That shared contract is the defining test. If you can describe every variant in a single sentence, such as 'given an order, return a price', you likely have an algorithm family. If each variant needs different inputs or produces fundamentally different results, you probably have separate responsibilities that only look similar. Forcing them under one interface produces awkward abstractions full of unused parameters and defensive null checks.

The second test is whether the choice varies over time or context. Strategy shines when the right algorithm depends on something outside the algorithm itself: a customer tier, a deployment environment, a feature flag, a file size. Java's Comparator is the canonical example. The sorting routine in Collections.sort never changes, yet each caller supplies whatever ordering rule it needs. The point of variation has been pulled out and made explicit, and the sorting code never has to know about it.

Knowing when not to apply the pattern matters just as much. Two branches that have been stable for five years do not need an interface, three classes, and a factory. Martin Fowler's guidance on refactoring applies here: introduce the abstraction when the duplication or the pressure to change is real, not merely imaginable. A conditional that never grows is cheaper than a pattern that never pays back. Speculative flexibility is a cost every future reader pays, whether or not the flexibility is ever used.

Takeaway

An algorithm family is defined by a shared contract and a real reason to vary. Without both, you have conditional logic dressed up as architecture.

What Encapsulation Buys You

Once you have identified the family, each variant moves into its own class that implements a common interface. For example, a ShippingCalculator interface might have GroundShipping, AirShipping, and FreightShipping implementations. The original method shrinks to a single delegating call. The sprawling conditional disappears. The decision has not vanished. It has moved to where it belongs.

Testing is the most immediate win. You can verify each strategy in isolation with focused inputs, without building the full context class or working around unrelated branches. You can test the context itself with a simple stub strategy, which confirms that it delegates correctly. Instead of a large grid of combined test cases, you get small, independent suites that fail precisely when something specific breaks. Diagnosing a regression becomes a matter of reading one class rather than tracing a dozen paths.

Reuse follows naturally. A strategy that captures a retry policy, a tax rule, or a serialisation format is no longer welded to one caller. Other components can adopt it without copying logic. Adding a new variant becomes an additive change: write a class, register it, and you are done. Existing strategies stay untouched, which is the Open/Closed Principle working as intended. Code review gets simpler too, because the diff is confined to new code.

The costs deserve an honest mention. The pattern adds types to the system, and readers must now look through an interface to find concrete behaviour. Clients or configuration must know enough about the available strategies to choose one. In languages with first-class functions, a full class hierarchy may be overkill. A lambda or function reference often expresses a strategy more economically. The principle, isolating the algorithm that varies, matters more than the ceremony used to express it.

Takeaway

Strategy does not eliminate decisions. It moves them. The payoff comes from separating the question of which algorithm to use from the work each algorithm performs.

How Clients Choose a Strategy

Encapsulating the algorithms solves only half the problem. Something still has to decide which strategy a client receives, and that mechanism shapes how flexible, testable, and understandable the system becomes. Three approaches dominate in practice, and each suits a different kind of variation. Choosing the wrong one quietly undoes much of the benefit the pattern was meant to deliver.

Configuration fits choices that vary by environment or deployment. A caching strategy might be in-memory during development and distributed in production, selected from a config file or environment variable at startup. The decision is made once, outside the code, and operators can change behaviour without a rebuild. The risk is that invalid configuration only shows up at runtime. Validate it eagerly and fail fast during startup rather than on the first unlucky request.

Dependency injection suits choices that are structural. The context class declares that it needs a PaymentProcessor and receives one through its constructor. A composition root, the single place where the application wires its objects together, or a container such as Spring or .NET's built-in DI, supplies the concrete implementation. The context never names a concrete class. That makes substitution in tests trivial, keeps the dependency graph explicit, and lets you see the whole system's wiring in one place.

Runtime conditions apply when the right algorithm depends on the data itself: a file's size, a user's region, a request's priority. Here the selection logic has to live somewhere, and the disciplined answer is a dedicated factory or resolver, often backed by a map from keys to strategies. This keeps the conditional in one small, well-tested place instead of scattering it across the codebase. Watch for a resolver that slowly grows back into the very conditional you set out to remove.

Takeaway

Ask when the decision can be made and who should make it. Choices fixed at deployment belong in configuration, structural choices belong in the composition root, and data-driven choices belong in a single, visible resolver.

The Strategy pattern endures because it addresses a problem that never goes away. Software must support multiple ways of doing the same thing, and those ways multiply over time. Handled carelessly, that growth produces brittle conditionals. Handled deliberately, it produces a system that extends cleanly.

A few guidelines capture the discipline. Introduce strategies when the variants share a contract and the pressure to change is real. Keep each strategy focused and independently testable. Prefer functions over class hierarchies when your language allows it. Match the selection mechanism to when the decision is made and who makes it.

Above all, treat the point of variation as a design decision worth naming. When the code makes clear what changes and what stays fixed, the next developer can extend it with confidence rather than fear.