Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last lesson moved a database dependency out of a service and supplied it through a constructor. The service still had one fixed way to make its status decision. What if the business rule itself has several interchangeable implementations?
In this lesson, you’ll refactor a Java shipping-quote service so it no longer contains a growing set of branches for different delivery methods. You’ll implement the Strategy pattern, select a strategy for each request, and test both the pricing rules and the selection behavior. Allow about 40 minutes, including time to type and run the code.
Recognize the behavior that varies
Suppose a checkout service quotes shipping in cents from a package’s weight in grams. Its current rules are:
- Standard: 500 cents plus 100 cents for each whole or partial kilogram.
- Express: 900 cents plus 250 cents for each whole or partial kilogram.
A first implementation might put both rules in one method:
long quoteCents(ShippingMethod method, int weightGrams) {
if (weightGrams <= 0) {
throw new IllegalArgumentException("Weight must be positive");
}
long kilograms = (weightGrams + 999L) / 1000;
return switch (method) {
case STANDARD -> 500 + 100 * kilograms;
case EXPRESS -> 900 + 250 * kilograms;
};
}
The arithmetic rounds a positive weight up to a whole kilogram. For example, both 1,001 and 2,000 grams are billed as two kilograms. The 999L also makes the addition use long arithmetic, avoiding overflow when weightGrams is a large int.
This switch is not inherently bad. With two short, stable rules, it may be the clearest solution. Strategy becomes useful when the alternatives grow more substantial, change independently, require their own collaborators, or appear as repeated branches across a service. The refactoring target is specifically the varying shipping calculation—not every conditional in the application.
Watch Geekific’s The Strategy Pattern Explained and Implemented in Java for the same progression in a payment example:
The Strategy Pattern Explained and Implemented in Java | Behavioral Design Patterns | Geekific
Geekific’s payment example shows how branching business behavior becomes interchangeable implementations. Watch for the boundary between choosing a behavior and executing it.
Watch the branching problem, then the pattern definition. In the refactor, notice that the service calls a shared contract rather than naming a payment implementation. Use the class diagram to identify the context, interface, concrete strategies, and client. The video shows runtime switching with a setter; a setter is an option, not a requirement of Strategy.
The diagram below gives names to those roles. Our ShippingQuoteService will be the context; ShippingRate will be the strategy interface; standard and express rates will be concrete strategies. Application wiring will supply the available strategies.

Extract a shared contract and preserve each rule
The two calculations take the same input and produce the same kind of result. That gives us a small interface. Each implementation also declares which delivery method it supports so the service can find it.
The following declarations can live in separate Java files. Type them as you read; the important check is that neither concrete class needs to know about the other.
enum ShippingMethod {
STANDARD, EXPRESS
}
interface ShippingRate {
ShippingMethod method();
long quoteCents(int weightGrams);
}
final class StandardShippingRate implements ShippingRate {
@Override
public ShippingMethod method() {
return ShippingMethod.STANDARD;
}
@Override
public long quoteCents(int weightGrams) {
long kilograms = (weightGrams + 999L) / 1000;
return 500 + 100 * kilograms;
}
}
final class ExpressShippingRate implements ShippingRate {
@Override
public ShippingMethod method() {
return ShippingMethod.EXPRESS;
}
@Override
public long quoteCents(int weightGrams) {
long kilograms = (weightGrams + 999L) / 1000;
return 900 + 250 * kilograms;
}
}
These objects contain no per-request state. They can be reused for many quotes: the request-specific weight is passed to quoteCents, rather than stored in a field. Using integer cents also keeps this example away from floating-point money calculations.
Notice what has not been extracted. The requirement that a quote has a positive weight applies to every method, so the context can enforce it once. Each strategy owns only its method’s pricing rule. This separation helps prevent one rule from silently handling invalid inputs differently from another.
Make the context delegate rather than branch
A backend service usually handles many requests, potentially with different shipping methods. Giving a singleton service a mutable setStrategy field would let concurrent requests interfere with one another. Instead, supply its available strategies at construction and select one locally for each call.
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
final class ShippingQuoteService {
private final Map<ShippingMethod, ShippingRate> rates;
ShippingQuoteService(List<ShippingRate> availableRates) {
EnumMap<ShippingMethod, ShippingRate> selected =
new EnumMap<>(ShippingMethod.class);
for (ShippingRate rate :
Objects.requireNonNull(availableRates, "availableRates")) {
Objects.requireNonNull(rate, "rate");
ShippingMethod method =
Objects.requireNonNull(rate.method(), "method");
if (selected.putIfAbsent(method, rate) != null) {
throw new IllegalArgumentException(
"Duplicate rate for " + method);
}
}
this.rates = Map.copyOf(selected);
}
long quoteCents(ShippingMethod method, int weightGrams) {
Objects.requireNonNull(method, "method");
if (weightGrams <= 0) {
throw new IllegalArgumentException("Weight must be positive");
}
ShippingRate rate = rates.get(method);
if (rate == null) {
throw new IllegalArgumentException(
"No rate configured for " + method);
}
return rate.quoteCents(weightGrams);
}
}
Outside a framework, application wiring can construct it directly:
ShippingQuoteService quotes = new ShippingQuoteService(
List.of(new StandardShippingRate(), new ExpressShippingRate()));
long standard = quotes.quoteCents(ShippingMethod.STANDARD, 1500);
long express = quotes.quoteCents(ShippingMethod.EXPRESS, 1500);
// standard is 700 cents; express is 1400 cents
Trace the standard call: the service validates the common input, looks up STANDARD, and calls quoteCents through ShippingRate. Java dispatches that call to StandardShippingRate. The service does not execute either pricing formula itself.
Selection has not disappeared. The request still supplies a ShippingMethod, and the map still chooses an implementation. What disappeared is the branch containing the business algorithms. The constructor also detects two strategies claiming the same method instead of silently allowing the last one to win.
This connects directly to the previous lesson: constructor injection supplies the collaborators, while Strategy describes how the service delegates a varying behavior to interchangeable collaborators. You can apply constructor injection without Strategy, and Strategy does not require Spring.
In a Spring Boot application, the two implementations could be component beans and ShippingQuoteService a service bean. Spring can then inject the list of ShippingRate beans into its sole constructor. The service design stays the same; only assembly changes. If you add another rate, you must still register it in non-Spring wiring—or provide a corresponding bean in Spring—and make its delivery method representable at the API boundary. “Open for extension” does not mean nothing anywhere changes.
Verify the rules and the delegation separately
A useful test suite answers two different questions: Does each strategy calculate correctly? And does the context choose and call the right one? The first is a straightforward rule test. The second protects the architecture of the refactor.
For a small JUnit 5 check of the concrete rules:
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class ShippingRateTest {
@Test
void roundsPartialKilogramsUp() {
ShippingRate standard = new StandardShippingRate();
ShippingRate express = new ExpressShippingRate();
assertEquals(600, standard.quoteCents(1000));
assertEquals(700, standard.quoteCents(1001));
assertEquals(1150, express.quoteCents(1000));
assertEquals(1400, express.quoteCents(1001));
}
}
The context can be tested without relying on either formula. Supply a tiny test strategy whose result is deliberately recognizable:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.List;
import org.junit.jupiter.api.Test;
class ShippingQuoteServiceTest {
@Test
void delegatesToTheStrategyForTheRequestedMethod() {
ShippingRate standard = new ShippingRate() {
@Override
public ShippingMethod method() {
return ShippingMethod.STANDARD;
}
@Override
public long quoteCents(int weightGrams) {
return weightGrams + 42L;
}
};
ShippingQuoteService service =
new ShippingQuoteService(List.of(standard));
assertEquals(1542,
service.quoteCents(ShippingMethod.STANDARD, 1500));
assertThrows(IllegalArgumentException.class,
() -> service.quoteCents(ShippingMethod.EXPRESS, 1500));
assertThrows(IllegalArgumentException.class,
() -> service.quoteCents(ShippingMethod.STANDARD, 0));
assertThrows(IllegalArgumentException.class,
() -> new ShippingQuoteService(
List.of(standard, standard)));
}
}
Run the tests after typing the implementation. If the 1001-gram assertions fail, inspect the rounding boundary. If the missing-express assertion fails, inspect how the map is built or how an unconfigured method is handled. These checks exercise behavior that is easy to miss when a refactor only verifies the two happy-path prices.
In an interview, you can summarize the decision this way: “I extracted the shipping calculations behind a common interface, supplied the implementations to the service, and made the service select and delegate per request. Common validation stays in the service; each strategy owns one pricing rule. I would keep the switch if the rules remained tiny and stable.” That last sentence matters: the goal is maintainable code, not a pattern applied automatically.
Key takeaways
Strategy packages alternative business behaviors behind one contract. The context selects and delegates; concrete strategies implement the varying rules. For a multi-request backend service, per-call selection from supplied, stateless strategies avoids a shared mutable “current strategy.” Test each rule at its boundaries, then test that the context delegates and rejects invalid configuration.
Next, you’ll look at Builder and Factory—patterns concerned with creating objects rather than choosing which behavior an existing service executes.