Encapsulating interchangeable algorithms behind a common interface, swappable at runtime — and the maintainability argument for choosing it over an if/else chain.
Published September 23, 2026
Strategy encapsulates a family of interchangeable algorithms behind one interface, so the code using them doesn't need to know or care which specific one is active.
interface PricingStrategy { double calculate(Order order); }
class RegularPricing implements PricingStrategy {
public double calculate(Order order) { return order.subtotal(); }
}
class BulkDiscountPricing implements PricingStrategy {
public double calculate(Order order) { return order.subtotal() * 0.9; }
}
class SeasonalPricing implements PricingStrategy {
public double calculate(Order order) { return order.subtotal() * 0.85; }
}
class Checkout {
private final PricingStrategy strategy; // injected — chosen by the caller, not hard-coded here
Checkout(PricingStrategy strategy) { this.strategy = strategy; }
double total(Order order) { return strategy.calculate(order); }
}
Checkout regular = new Checkout(new RegularPricing());
Checkout bulk = new Checkout(new BulkDiscountPricing());
// Same Checkout class, different pricing behavior — chosen at construction, swappable at runtime
// Without Strategy — every new pricing rule means editing this method
double total(Order order, String pricingType) {
if (pricingType.equals("REGULAR")) return order.subtotal();
else if (pricingType.equals("BULK")) return order.subtotal() * 0.9;
else if (pricingType.equals("SEASONAL")) return order.subtotal() * 0.85;
throw new IllegalArgumentException("Unknown pricing type");
}
This is the exact same OCP violation shown in Single Responsibility & Open/Closed's payment-method example: every new pricing rule means opening this method, adding a branch, and re-testing all existing branches for regression risk. With Strategy, adding a new pricing rule means writing one new class — the Checkout class (and anything else consuming PricingStrategy) never changes. This is the specific argument to give an interviewer who asks "why not just use if/else": it isn't about the if/else being slow, it's about every new case requiring a change to code that was already tested and shipped.
interface SortStrategy { void sort(int[] data); }
class BubbleSort implements SortStrategy { public void sort(int[] data) { /* O(n^2), simple */ } }
class QuickSort implements SortStrategy { public void sort(int[] data) { /* O(n log n) average */ } }
class Sorter {
private SortStrategy strategy;
void setStrategy(SortStrategy strategy) { this.strategy = strategy; } // can even swap at runtime, not just construction
void sort(int[] data) { strategy.sort(data); }
}
A Sorter could pick BubbleSort for tiny, nearly-sorted inputs and QuickSort for large ones — the point isn't that one algorithm is universally better, it's that the choice of algorithm is isolated from the code that needs sorting done.
Q: How is Strategy different from just passing a lambda/functional interface?
A: For simple, single-method behavior, a Function/Comparator-style lambda often is a lightweight Strategy — Java's functional interfaces are Strategy without the ceremony of a named class. Strategy as a named-class pattern earns its keep when the "algorithm" needs multiple methods, internal state, or a constructor with configuration — beyond what a bare lambda can express.
Q: Isn't Strategy the same pattern as Bridge? A: Structurally similar (both compose a reference instead of inheriting), but different intent and scope: Strategy typically swaps one algorithm at a single decision point; Bridge decouples two entire class hierarchies so both can evolve independently over an object's whole lifetime (see Bridge & Flyweight).
Q: Where would you put the logic that decides WHICH strategy to use?
A: That decision itself is exactly what a Factory (see Factory & Abstract Factory) is for — Strategy and Factory commonly pair up: a factory method takes a type/condition and returns the appropriate PricingStrategy instance, keeping the selection logic in one place too.