Strategy Pattern & Dependency Inversion
An interface, two implementations, constructor injection, and still no Strategy and no inversion. These two ideas are related in a precise way that most write-ups blur, and the blur is expensive: it produces codebases full of interfaces that cost maintenance and buy nothing. This page draws the line sharply enough to settle an argument in a code review.
The claim under test
"We use the Strategy pattern here." It is one of the most frequently made and least frequently earned claims in enterprise Java, and unlike most architectural disputes it is decidable in about forty seconds. You do not need to agree on taste. You need to look at two things.
- Test 1: the Context field. Does the class that uses the algorithm declare its dependency as the interface, or does it name a concrete class? A Context that names an implementation is not a Context; it is a caller.
- Test 2: the type test. Is there an
instanceof, agetClass(), aswitchover a type discriminator, or anif/elsechain choosing behaviour inside the Context? If so, the branching is the algorithm selection, and you have conditional dispatch wearing an interface as a hat.
// Test 1 fails: the field names a concrete class.
private final RayCastingCollisionAlgorithm rayCasting;
// Test 1 passes: the field names the abstraction. This class cannot know,
// and has no way to ask, which implementation it was handed.
private final CollisionAlgorithm collisionAlgorithm;
// Test 2 fails: the Context is branching on the runtime type of its input.
if (area instanceof Polygon p) { return rayCasting.contains(p, position); }
// Test 2 passes: one call, dispatched by the JVM rather than by this class.
return collisionAlgorithm.contains(area, position);
Both tests are about where knowledge lives. In a real Strategy, the knowledge of which algorithm applies lives outside the Context: in the caller, in a factory, in configuration, in the wiring. Inside the Context there is one call and no opinion.
GoF Strategy, structurally
Strategy comes from the Gang of Four, or GoF: the four authors of Design Patterns, published in 1994. Their definition is short enough to hold in one hand, and every clause of it is load-bearing.
Strategy is a behavioral design pattern that lets you define a family of algorithms, put each of them into separate classes, and make their objects interchangeable.
Note the three obligations packed into one sentence: a family (so, more than one), separate classes (so, not branches in a method), and interchangeable objects (so, a common supertype the user of them can hold). The same page is explicit about the Context's field and about who does the choosing:
The original class, called context, must have a field for storing a reference to one of the strategies.
… The context isn't responsible for selecting an appropriate algorithm for the job. Instead, the client passes the desired strategy to the context.
Wikipedia states the consequence of that field being typed as the interface, which is the whole reason the pattern is worth the classes it costs:
Context refers to the Strategy interface for performing an algorithm (strategy.algorithm()), which makes Context independent of how an algorithm is implemented.
(1) A Strategy interface. (2) More than one ConcreteStrategy: a family of one is not a family. (3) A Context whose field is typed as the interface. (4) Selection performed outside the Context, so that adding a strategy never edits it. Miss (3) and you have coupled code with extra files. Miss (4) and you have conditional dispatch.
Dependency Inversion: what "inversion" actually means
Robert Martin's principle is usually quoted in its first half and forgotten in its second. Both halves matter, and it is the second one that makes it a principle rather than a style preference:
High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
Read literally, that is satisfiable by any interface anywhere. It is not. The word being inverted names something specific, and the same article says exactly what:
…the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed…
In a direct application of dependency inversion, the abstracts are owned by the higher/policy layers. This architecture groups the higher/policy components and the abstractions that define lower services together in the same package.
The abstraction is owned by the consumer. That is the sentence almost every tutorial skips, and without it the Dependency Inversion Principle (DIP) degenerates into "use interfaces". The interface is not a neutral object floating between two modules. It is a statement of what the high-level module requires, written in the high-level module's vocabulary, and it belongs in the high-level module's package. The implementation then reaches upward to satisfy it. Martin Fowler catalogued the mechanic separately:
Defines an interface in a separate package from its implementation.
… This way a client that needs the dependency to the interface can be completely unaware of the implementation.
geofencing.algorithm and every
arrow still points down: that is indirection, not inversion, and it is the diagram most
codebases are actually shaped like.
The two are not siblings. Strategy is a concrete application of DIP: the Context is the high-level module, the Strategy interface is the abstraction it owns, and each ConcreteStrategy is a detail depending on it. DIP is the general rule about the direction of source-code dependencies; Strategy is what it looks like when the thing being inverted is an algorithm and you want to swap it.
DI is not DIP, and neither is IoC
Three terms, routinely used as synonyms, describing three different things: dependency injection (DI), the Dependency Inversion Principle (DIP), and inversion of control (IoC). Fowler had to coin one of them precisely because the existing name was too broad:
As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.
| Term | What it governs | Satisfied by | Can be present without the others? |
|---|---|---|---|
IoCIoCInversion of control. The framework decides when your code runs and calls it, instead of your code driving the sequence and calling the framework. |
Who calls whom: the framework calls you | Any framework lifecycle, template method, callback | Yes |
DIDIDependency injection. An object is handed the collaborators it needs, usually as constructor parameters, instead of creating them itself. |
How an object receives its collaborators | A constructor parameter, container optional | Yes, and usually is |
DIPDIPThe Dependency Inversion Principle. Source dependencies point towards an abstraction that the consumer owns, so the high-level code does not depend on low-level detail. |
Which direction source dependencies point | An abstraction owned by the consumer | Yes, new satisfies it fine |
The row that surprises people is the last one. A hand-written class that takes an interface
in its constructor and is instantiated with new obeys DIP completely and uses no
container at all. Conversely, and this is the shape of the production defect further down,
a class can be a perfectly wired Spring @Service using textbook constructor
injection and still violate DIP on every line, because what it is injected with are
concrete types.
The "cosmetic DIP" smell
Here is the pattern to learn to see: an interface PaymentService, one
implementation PaymentServiceImpl, both in package …payment, the
interface existing because a mocking library was easier to satisfy that way. Every DIP box is
ticked and nothing has been inverted. The consumer still depends on a type shipped by the
implementer; move the implementation and the interface moves with it; add a second
implementation and you discover the interface was shaped around the first one's internals.
| Question | Earns its place | Noise |
|---|---|---|
| Which package holds the interface? | The consumer's | The implementation's |
| Who chose its method names? | The caller's vocabulary | Mirrors the impl, method for method |
| What does the second implementation look like? | Named and plausible: a test double at a real boundary counts | Cannot be described |
| What breaks if you delete it? | A boundary the domain must not cross | One import line |
An interface at a true boundary does DIP even if production ships one impl, because the second implementation is the test double and it is a real, permanent one:
Clock: time is an external service- Filesystem, object storage, outbound HTTP
- Message broker, mail gateway, payment provider
- Anything whose real version is slow, non-deterministic, or costs money
OrderServiceImpl beside OrderService, in the same package, with no boundary between them. Mockito can mock the concrete class; the interface buys nothing and costs a second file, a second name and a permanent indirection in every jump-to-definition.
The tell is that you cannot name the second implementation, and neither can anyone else on the team.
A single implementation is not automatically a smell, and this matters for the evidence
below. LinearSplitStrategy has no sibling in the codebase, yet it is not
cosmetic: linear names one member of a documented family of R-Tree node-splitting
algorithms, with quadratic and R*-tree splits as the others, and the interface is owned by
the tree that consumes it. PaymentServiceImpl names nothing; the suffix is an
admission that no second member was ever conceived. Ask what the name distinguishes.
If it distinguishes nothing, the interface is decoration.
What this looked like in production
A geofencing service I wrote start to finish contained, roughly two hundred lines apart, a clean use of the pattern and a broken one. They are worth studying together, because the pair is the actual lesson. Both were mine. The defect described below has since been repaired, using the implementation shown further down this page.
The clean instance. A hand-written R-Tree spatial index holds its node-splitting policy behind an interface:
- The field is
private final SplitStrategy splitStrategy: the interface, never the implementation. - It is constructor-injected, with a null guard, by plain Java. No container is involved, which is a neat demonstration that DIP is a source-dependency property, not a framework feature.
LinearSplitStrategyis the concrete implementation, and the tree contains no type test anywhere: noinstanceof, nogetClass(), no branch on strategy identity.
This is the shape to copy. Its one honest limitation, stated rather than hidden: only one concrete strategy currently exists, so interchangeability here is structural rather than exercised. The abstraction would survive a second implementation. It has simply never had to.
The broken sibling, in the same service. An interface CollisionAlgorithm
exists. RayCastingCollisionAlgorithm implements CollisionAlgorithm. But its
sibling HaversineDistanceCollisionAlgorithm is a @Component that
carries no implements clause at all. The abstraction was written, one
class was attached to it, and the second was never connected.
Everything downstream follows mechanically. The context class
GeofencingEvaluationService cannot declare
List<CollisionAlgorithm>, because the two classes share no supertype. It
therefore injects both concrete types side by side and selects between them with a
branch. That is a broken Strategy and a DIP violation in the same constructor, sitting
directly beside the abstraction that would have fixed both.
The instructive part is not the defect but its neighbour. I got it exactly right in one
file and left it unfinished two hundred lines away, in the same service. I was learning
dependency inversion while writing the collision code, and I did not yet know the rule
the first half of this page states. That is how these defects actually occur. Not
through ignorance of the pattern, but through an abstraction that is started and not
carried through, at the point where the second implementation arrives under time
pressure and implements CollisionAlgorithm is one keystroke that nothing
forces anyone to type. Nothing forced it here, so it was not typed, and the compiler
had no opinion. The fix was one clause and one deleted branch.
implements clause is a compiler-silent architecture failure
Nothing fails. The service compiles, the tests pass, Spring wires both beans happily. The only symptom is a constructor signature that names two concrete classes, which is exactly why Test 1 is worth running by hand in review. No linter reports it, because at the level of the type system nothing is wrong; what is wrong is the direction of a dependency, and that is a question about intent.
Doing it properly
Three tabs: the defect as it was, the repair that shipped, and the reference implementation from the same service. Read the second and third tabs as the model to copy, and the first as the shape to recognise and avoid. The repair was not "add an interface", because the interface already existed. It was finishing it, then deleting the branch that only existed because it was unfinished.
package geofencing;
import geofencing.algorithm.HaversineDistanceCollisionAlgorithm;
import geofencing.algorithm.RayCastingCollisionAlgorithm;
import org.springframework.stereotype.Service;
/**
* The abstraction already exists, one package away. This class never names it.
* Constructor injection is used correctly, so this is textbook DI, and it is
* not DIP, because every dependency it declares is a concrete type.
*/
@Service
public class GeofencingEvaluationService {
private final RayCastingCollisionAlgorithm rayCasting;
private final HaversineDistanceCollisionAlgorithm haversine;
public GeofencingEvaluationService(RayCastingCollisionAlgorithm rayCasting,
HaversineDistanceCollisionAlgorithm haversine) {
this.rayCasting = rayCasting;
this.haversine = haversine;
}
public boolean isInside(Geometry area, Point position) {
// Conditional dispatch, not Strategy. A third geometry means editing
// this method, precisely what a Strategy Context never has to do.
if (area instanceof Polygon polygon) {
return rayCasting.contains(polygon, position);
} else if (area instanceof Circle circle) {
return haversine.contains(circle, position);
}
throw new IllegalArgumentException("Unsupported geometry: " + area);
}
}
package geofencing;
/**
* DIP: the abstraction lives in the package that CONSUMES it, not the one
* that implements it. Move this file into geofencing.algorithm and you have
* indirection again. The arrow points down and nothing has been inverted.
*/
public interface CollisionAlgorithm {
/** What this algorithm is for. Domain data, not a Spring bean name. */
GeometryKind supports();
boolean contains(Geometry area, Point position);
}
// ---------------------------------------------------------------------------
package geofencing.algorithm;
import geofencing.CollisionAlgorithm; // the low-level package depends UPWARD
import geofencing.GeometryKind;
import org.springframework.stereotype.Component;
@Component
public class HaversineDistanceCollisionAlgorithm implements CollisionAlgorithm {
// That clause is the entire fix. RayCastingCollisionAlgorithm already had it.
@Override
public GeometryKind supports() {
return GeometryKind.CIRCLE;
}
@Override
public boolean contains(Geometry area, Point position) { … }
}
// ---------------------------------------------------------------------------
package geofencing;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
@Service
public class GeofencingEvaluationService {
private final Map<GeometryKind, CollisionAlgorithm> byKind;
/**
* Spring hands over every bean implementing the interface. The map is
* built here rather than injected as Map<String, CollisionAlgorithm>,
* because Spring keys that map by BEAN NAME, a container detail, not a
* domain fact. Keying on supports() keeps selection inside the domain.
*/
public GeofencingEvaluationService(List<CollisionAlgorithm> algorithms) {
this.byKind = algorithms.stream().collect(Collectors.toUnmodifiableMap(
CollisionAlgorithm::supports, Function.identity()));
}
public boolean isInside(Geometry area, Point position) {
var algorithm = byKind.get(area.kind());
if (algorithm == null) {
throw new IllegalStateException("No CollisionAlgorithm for " + area.kind());
}
return algorithm.contains(area, position); // no type test, no branch
}
}
package spatial.rtree;
import java.util.List;
import java.util.Objects;
/**
* Node splitting is the one decision an R-Tree genuinely has to make, and the
* one the literature disagrees about: linear-cost, quadratic-cost and the
* R*-tree variant are documented members of the same family. The tree owns
* this interface: a split algorithm is never allowed to own the tree.
*/
public interface SplitStrategy {
<T> Split<T> split(List<Entry<T>> entries, int minEntries);
}
/** A split is a value, so it is a record, not a service and not a bean. */
public record Split<T>(List<Entry<T>> left, List<Entry<T>> right) {
public Split {
left = List.copyOf(left);
right = List.copyOf(right);
}
}
// ---------------------------------------------------------------------------
public final class RTree<T> {
// The Context holds the INTERFACE. There is no reference to a concrete
// strategy anywhere in this class: not in a field, not in a branch.
private final SplitStrategy splitStrategy;
private final int maxEntries;
public RTree(SplitStrategy splitStrategy, int maxEntries) {
this.splitStrategy = Objects.requireNonNull(splitStrategy, "splitStrategy");
this.maxEntries = maxEntries;
}
/** Selection lives with the caller, and needs no container to do it. */
public static <T> RTree<T> withDefaults() {
return new RTree<>(new LinearSplitStrategy(), 8);
}
private void overflow(Node<T> node) {
Split<T> split = splitStrategy.split(node.entries(), maxEntries / 2);
node.replaceWith(split.left(), split.right());
}
}
Once the interface is finished, the Context still has to pick an implementation. These are the three ways to do that, in descending order of preference. The repair in the second tab uses the first one.
-
Inject
List<T>and index it yourself on a method the interface declares, such assupports(). The key is domain data, so it survives a rename of the bean. -
Inject
Map<String, T>. Spring fills it with every implementation, keyed by bean name. This is fine when the key genuinely is a configuration string, but it couples selection to Spring’s naming. - Use a factory when construction needs arguments the container does not have.
Not on the list: instanceof, getClass(), or a switch
in the Context. Each of those puts the Context back in the business of knowing which
implementations exist, which is the branch the repair deleted.
Strategy vs plain conditional dispatch
The honest counterweight to everything above: not every branch wants to be a class. The
Strategy pattern's own documentation lists this as its first drawback, and it is the
sentence to quote back at anyone converting a two-line if into three files.
If you only have a couple of algorithms and they rarely change, there's no real reason to overcomplicate the program with new classes and interfaces that come along with the pattern.
- The branch set is closed and stable: two arms that have not changed in three years and are not going to.
- Each arm is a few lines with no dependencies of its own.
- The condition is on a value, not a type: a boolean flag, a threshold, a null check.
- The arms share almost all their context and separating them would mean passing six parameters into each strategy.
Converting this costs three files and buys an indirection. Don't.
- The branch grows: a new arm arrives every time the domain gains a case.
- The condition tests a type:
instanceof,getClass(), or a discriminator enum that shadows a class hierarchy. - Each arm needs its own collaborators, so the Context accumulates dependencies it uses one-tenth of the time.
- The same branch is duplicated in two or more places and they have already drifted.
- The set must be extensible without recompiling the Context: plugins, config-driven behaviour, per-tenant rules.
Not "how many branches are there" but "what does the next branch cost?" If adding a case means opening a file that has nothing to do with that case, editing a method other cases depend on, and re-testing all of them, the conditional has become a coordination point and Strategy pays for itself. If adding a case is a two-line edit nobody fears, the conditional is fine and the pattern is overhead.
Canonical sources
- Refactoring.guru: Strategy · intent, the Context's field, who selects, and the cons quoted above.
- Wikipedia: Strategy pattern · the structural statement that the Context refers to the interface and is thereby independent of the implementation.
- Wikipedia: Dependency inversion principle · both clauses, what is reversed, and the ownership of the abstractions by the higher layers.
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern · why "Inversion of Control" was too generic a name, and what DI actually removes.
- Martin Fowler: Separated Interface · the packaging mechanic that makes ownership concrete.