Skip to content
QSWEQB
mediumConceptDesignMidSeniorStaff

When would you use the strategy pattern instead of inheritance?

Use strategy when behaviour varies along an axis separate from what the object is. Composition lets two axes combine without one subclass per pair, lets the behaviour be swapped at runtime, and lets the algorithm be tested without constructing its host.

5 min readUpdated 2026-07-26

What the interviewer is scoring

  • Does the candidate reach for a second axis of variation rather than arguing composition on principle
  • Whether they can count the class explosion out loud instead of gesturing at it
  • That they mention runtime swappability, which inheritance cannot offer at all
  • Whether the testability argument names what becomes testable in isolation, not just "it is more testable"
  • Does the candidate concede that in a language with lambdas the ceremony is often unnecessary

Answer

Two different kinds of binding

Inheritance binds behaviour to identity at compile time. When you write class PdfReport extends Report, you have declared that this object is a PDF report for its entire lifetime, and you have spent the one superclass slot the language gives you. Strategy binds behaviour to a field at construction time or later, so the object holds a reference to the algorithm rather than being defined by it.

That difference sounds academic until behaviour varies along more than one axis, which is when it stops being academic and starts producing classes.

Counting the subclass explosion

Take a reporting job that varies in two independent ways: how the data is rendered, and where the output goes. With inheritance you must pick one axis for the class hierarchy and duplicate the other across it.

abstract class ReportJob {
    abstract byte[] render(Data d);
    abstract void deliver(byte[] bytes);
    final void run(Data d) { deliver(render(d)); }
}

class CsvEmailReport extends ReportJob { /* render CSV, deliver by email */ }
class CsvS3Report    extends ReportJob { /* render CSV again, deliver to S3 */ }
class PdfEmailReport extends ReportJob { /* render PDF, deliver by email again */ }
class PdfS3Report    extends ReportJob { /* ... */ }

Two renderers and two sinks give four classes. Add XLSX and an SFTP sink and you are at nine; three renderers and four sinks is twelve. Every one of those twelve duplicates either a rendering routine or a delivery routine, so a bug fix in the S3 upload path has to be applied in three places, and a reviewer has no way to tell from the class list which pairs actually exist and which nobody ever asked for. The count grows as the product of the axes, and the interesting code grows as their sum.

The same design with composition

interface Renderer { byte[] render(Data d); }
interface Sink { void write(byte[] bytes); }

final class ReportJob {
    private final Renderer renderer;
    private final Sink sink;

    ReportJob(Renderer renderer, Sink sink) {
        this.renderer = renderer;
        this.sink = sink;
    }

    // The only method that knows the two steps are related.
    void run(Data d) { sink.write(renderer.render(d)); }
}

var job = new ReportJob(new PdfRenderer(), new S3Sink(bucket));

Three renderers plus four sinks is seven small classes and one ReportJob, and any of the twelve combinations is now expressible without writing anything new. You have replaced a product with a sum. This is what "favour composition over inheritance" means concretely: not that inheritance is bad, but that combining independent behaviours is what object references are for and what a class hierarchy is structurally unable to do.

Swapping at runtime

An object cannot change its class after construction, so anything inheritance encodes is fixed for the object's life. A strategy field can be reassigned, which is the only way to express behaviour that genuinely changes while the object stays the same. A pricing engine that switches to a surge algorithm during peak hours, a connection that downgrades its retry policy after repeated failures, a sort order the user picks from a dropdown — all of these need the behaviour to be data, and Collections.sort(list, comparator) is exactly that pattern in the JDK. Comparator is a strategy: the ordering is passed in, not inherited by the list.

What becomes testable

With the inheritance version you cannot test PDF rendering without instantiating a ReportJob subclass that also delivers somewhere, so the test either performs real delivery or you invent a PdfNoopReport subclass that exists only for tests. With the composed version PdfRenderer is a class with one method, no superclass, and no dependencies; you construct it, call it, and assert on the bytes. The delivery side is tested the same way, and ReportJob.run is tested against two trivial fakes to check only that it renders before it writes. Three focused tests replace one integration test that could fail for four reasons.

In a language with first-class functions, this is often just a function

Be honest about this in the interview, because it is a genuine credibility signal. Sink has one method, so in Java it is a functional interface and the call site can be new ReportJob(new PdfRenderer(), bytes -> s3.put(key, bytes)). In Python, Go, Kotlin, or TypeScript you would pass a function and never declare the interface at all. The pattern was catalogued in 1994 for C++ and Smalltalk, and C++ had no lambdas until C++11; a large part of the class scaffolding in the original description is a workaround for the absence of closures rather than an insight about design.

Named strategy classes still earn their place when the strategy carries its own configuration or state, when it needs a stable name so it can be resolved from a registry or injected by a container, or when it has more than one method. Otherwise a function is the strategy pattern with less typing, and calling it "the strategy pattern" is a description of the shape, not a requirement to build a hierarchy.

Where the argument stops working

The reason to prefer inheritance is not familiarity, it is that subclasses vary in what they are along a single axis and share substantial state — an AbstractHttpRequest hierarchy, or a sealed set of AST node types you exhaustively switch over. Reaching for strategy there produces a hollow class delegating everything to a field.

More usefully, there is a test for whether an extraction is real: look at what the strategy needs. If Renderer.render requires the job's connection pool, its retry counter, and its half-built output buffer, you have not extracted a strategy — you have written a subclass whose this arrives through the parameter list, and every future change to the host's internals will ripple into every strategy. A strategy is worth extracting only when its interface can stay narrow, which is another way of saying only when the axis of variation was genuinely independent in the first place. Candidates who recite "composition over inheritance" without this check will happily refactor a coherent class into six classes and an interface with a five-argument method, which is worse than what they started with.

The question to answer out loud is not "is inheritance bad" but "how many axes vary, and can the varying part be described without reaching into the host". Two independent axes make strategy almost automatic; one axis and shared state usually means the subclass was right.

Likely follow-ups

  • How does strategy differ from template method, given both let subclasses vary one step?
  • What do you do when the strategy needs data that only the host object holds?
  • When is a named strategy class worth more than a lambda?
  • How would you select a strategy from configuration without a switch statement in the caller?

Related questions

Further reading

strategy-patterncompositioninheritancebehavioural-patternsrefactoring