June 17, 2026 in patterns4 minutes
A intermediate-level guide to Strategy Pattern: before-and-after java code and diagrams for a CS student.
Imagine you are building a shipping calculator for an e-commerce platform. Initially, you only offer standard shipping, so the math is simple. But business grows. Now you need express and overnight options. Every time a new shipping method is added, you have to open up your existing ShippingCalculator class and add another else if block inside the calculateCost method.
This pattern creates “Conditional Complexity,” a code smell where a single method becomes a dumping ground for every possible variation of a business rule. This design violates the Open/Closed Principle, which states that software entities should be open for extension but closed for modification. In other words, you shouldn’t have to rewrite and re-test your core calculator class just because the marketing department added a “Super Express” tier.
public class ShippingCalculator {
private final String shippingMethod;
public ShippingCalculator(String shippingMethod) {
this.shippingMethod = shippingMethod;
}
public double calculateCost(double weight) {
// Conditional complexity: adding a new method requires modifying this class
if (shippingMethod.equals("STANDARD")) {
return weight * 1.5;
} else if (shippingMethod.equals("EXPRESS")) {
return weight * 3.0 + 5.0;
} else if (shippingMethod.equals("OVERNIGHT")) {
return weight * 5.0 + 15.0;
} else {
throw new IllegalArgumentException("Unknown shipping method");
}
}
}The logic is tightly coupled to specific string names, making the code brittle and difficult to maintain as the list of methods grows:
flowchart TD
Start([calculateCost]) --> C1{method == STANDARD?}
C1 -- yes --> R1[return weight * 1.5]
C1 -- no --> C2{method == EXPRESS?}
C2 -- yes --> R2[return weight * 3 + 5]
C2 -- no --> C3{method == OVERNIGHT?}
C3 -- yes --> R3[return weight * 5 + 15]
C3 -- no --> E[throw exception]
To fix this, we use the Strategy Pattern. Think of a video game character who can switch between a sword and a bow. The character doesn’t have “sword logic” and “bow logic” hardcoded into their own attack() method via a massive if statement. Instead, the character has a single weapon slot. When they equip a sword, you put a Sword object in that slot; when they switch to a bow, you swap it for a Bow object. The character simply calls weapon.attack(), and Polymorphism—the ability of an object to take on many forms—ensures the correct math happens at runtime.
In our Java refactor, we extract each shipping calculation into its own class that implements a common interface. The ShippingCalculator no longer contains any math; it simply holds a reference to a ShippingStrategy and asks it to do the work. This is known as Delegation.
public interface ShippingStrategy {
double calculate(double weight);
}
class StandardShipping implements ShippingStrategy {
@Override
public double calculate(double weight) { return weight * 1.5; }
}
class ExpressShipping implements ShippingStrategy {
@Override
public double calculate(double weight) { return weight * 3.0 + 5.0; }
}
class OvernightShipping implements ShippingStrategy {
@Override
public double calculate(double weight) { return weight * 5.0 + 15.0; }
}
public class ShippingCalculator {
private final ShippingStrategy strategy;
public ShippingCalculator(ShippingStrategy strategy) {
this.strategy = strategy;
}
public double calculateCost(double weight) {
// Delegation: the calculator no longer cares about specific math logic
return strategy.calculate(weight);
}
}By using an interface, we have decoupled the “how” (the specific math) from the “when” (the request for a cost). The ShippingCalculator now relies on Composition—building complex objects by combining simpler ones—rather than hardcoded logic.
classDiagram
direction BT
StandardShipping ..|> ShippingStrategy : implements
ExpressShipping ..|> ShippingStrategy : implements
OvernightShipping ..|> ShippingStrategy : implements
ShippingCalculator o-- ShippingStrategy : uses
When the application runs, the ShippingCalculator delegates the calculation to whichever concrete implementation was provided during its construction. This interaction looks like this:
sequenceDiagram
participant C as ShippingCalculator
participant S as StandardShipping
C->>S: calculate(weight)
S-->>C: return cost
The Strategy Pattern is a powerful tool for managing behavioral variation, but it is not a silver bullet.
Use it when:
if-else or switch blocks.Avoid it when:
true/false toggle). Adding an interface, multiple classes, and dependency injection for a single line of logic is “over-engineering” and adds unnecessary boilerplate code.Demonstrating how the client swaps behaviors via constructor injection
ShippingCalculator standard = new ShippingCalculator(new StandardShipping());
double cost = standard.calculateCost(10.0); // 15.0
// We can swap behavior by injecting a different strategy
ShippingCalculator express = new ShippingCalculator(new ExpressShipping());