← All patterns

Strategy

Behavioral · view source on GitHub

Read this in: English · Português · Español

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

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

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);
    }
}

View full JaCoCo coverage report →