Category: Behavioral
The problem
A piece of behavior has several valid variants, and which one applies depends on some
runtime condition — the transport mode, the transaction type, the sorting order. The
tempting first implementation is a single method with a big if/else or switch over
that condition. It works until the third or fourth variant shows up, at which point the
method is long, every change risks breaking an unrelated branch, and adding a new variant
means editing code that already works instead of just adding new code next to it.
The solution
Extract each variant behind a common interface, and give the calling code a way to plug in whichever implementation applies — swappable at runtime, and each variant is a self-contained class that can be tested, read, and changed in isolation.
classDiagram
class Strategy {
<<interface>>
}
class ConcreteStrategyA
class ConcreteStrategyB
class Context {
-strategy
+setStrategy(s)
+execute()
}
Strategy <|.. ConcreteStrategyA
Strategy <|.. ConcreteStrategyB
Context --> Strategy
Classic example
classic/RouteStrategy
computes a route between two points; DrivingRouteStrategy,
WalkingRouteStrategy
and PublicTransportRouteStrategy
each apply a different detour factor, speed, and (for transit) a fixed wait time on top of
the same straight-line distance calculation. Navigator
is the context: it holds a strategy and delegates to it, and setStrategy(...) lets a caller
swap the travel mode for the same trip without touching Navigator itself.
NavigatorTest
checks both the per-strategy math and that swapping strategies actually changes the outcome
for an identical origin/destination pair.
Applied example: per-transaction-type fee calculation
applied/FeeCalculator
looks up a FeeCalculationStrategy
by TransactionType
instead of branching on it: PixFeeStrategy
is free (BACEN mandates free PIX between individuals), TedFeeStrategy
charges a flat fee regardless of amount, and BoletoFeeStrategy
charges a percentage with a minimum floor. This is precisely the scenario the pattern is for:
a real payment gateway adding a fourth transaction type later means adding one new strategy
class, not reopening a fee-calculation method that every existing transaction type already
depends on. FeeCalculatorTest
covers all three strategies plus the "unregistered type" failure case.
When not to use it
- If there's really only one variant today and no concrete plan for a second, a strategy interface is speculative abstraction — a plain method is clearer until the second variant actually shows up.
- If the variants share most of their logic and differ only in one or two steps, Template Method (fixing the skeleton, overriding the steps) is usually a better fit than Strategy (swapping the whole algorithm).
- Don't let the context class grow business logic that decides which strategy to use based on deep domain rules — if that selection logic gets complex, it deserves its own factory (see this repo's Factory Method / Abstract Factory modules once they land).
Test coverage
100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:
./gradlew :behavioral:strategy:jacocoTestReport
Report at behavioral/strategy/build/reports/jacoco/test/html/index.html.
Further reading
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — Chapter 5 formalizes Strategy.
- Parnas, D. L. (1972). "On the Criteria to Be Used in Decomposing Systems into Modules." Communications of the ACM, 15(12), 1053–1058. — the foundational information-hiding argument for why an algorithm variant belongs behind a stable interface (a module boundary) instead of inside a conditional that every caller has to know about.
- Liskov, B. (1987). "Data Abstraction and Hierarchy." OOPSLA '87 Addendum to the Proceedings,
ACM SIGPLAN Notices, 23(5). — the original statement of what became the Liskov
Substitution Principle: every concrete strategy must be swappable for
RouteStrategy/FeeCalculationStrategywithout changing the correctness of the code that calls it, which is exactly the contractNavigatorandFeeCalculatordepend on.
Unit tests
src/test/java/com/designpatterns/behavioral/strategy/classic/NavigatorTest.java
package com.designpatterns.behavioral.strategy.classic;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class NavigatorTest {
private final Location origin = new Location(0, 0);
private final Location destination = new Location(3, 4); // straight-line distance = 5km
@Test
void drivingRouteAppliesTheRoadDetourFactor() {
Navigator navigator = new Navigator(new DrivingRouteStrategy());
Route route = navigator.route(origin, destination);
assertThat(route.distanceKm()).isEqualTo(5 * 1.3);
assertThat(route.description()).isEqualTo("driving");
}
@Test
void swappingTheStrategyChangesTheRouteForTheSameTrip() {
Navigator navigator = new Navigator(new WalkingRouteStrategy());
Route walking = navigator.route(origin, destination);
navigator.setStrategy(new PublicTransportRouteStrategy());
Route transit = navigator.route(origin, destination);
assertThat(walking.distanceKm()).isNotEqualTo(transit.distanceKm());
assertThat(walking.estimatedMinutes()).isNotEqualTo(transit.estimatedMinutes());
}
}
src/test/java/com/designpatterns/behavioral/strategy/applied/FeeCalculatorTest.java
package com.designpatterns.behavioral.strategy.applied;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class FeeCalculatorTest {
private final FeeCalculator calculator = FeeCalculator.withDefaultStrategies();
@Test
void pixTransfersAreFree() {
assertThat(calculator.calculateFeeCents(TransactionType.PIX, 500_00L)).isZero();
}
@Test
void tedChargesAFlatFeeRegardlessOfAmount() {
assertThat(calculator.calculateFeeCents(TransactionType.TED, 100_00L)).isEqualTo(1000L);
assertThat(calculator.calculateFeeCents(TransactionType.TED, 50_000_00L)).isEqualTo(1000L);
}
@Test
void boletoChargesThePercentageFeeAboveTheMinimum() {
assertThat(calculator.calculateFeeCents(TransactionType.BOLETO, 100_000_00L)).isEqualTo(2_000_00L);
}
@Test
void boletoFallsBackToTheMinimumFeeForSmallAmounts() {
assertThat(calculator.calculateFeeCents(TransactionType.BOLETO, 1_00L)).isEqualTo(350L);
}
@Test
void rejectsAnUnregisteredTransactionType() {
FeeCalculator calculatorWithoutBoleto = new FeeCalculator(Map.of(TransactionType.PIX, new PixFeeStrategy()));
assertThatThrownBy(() -> calculatorWithoutBoleto.calculateFeeCents(TransactionType.BOLETO, 100L))
.isInstanceOf(IllegalArgumentException.class);
}
}