00:00

Strategy Design Pattern in Java

Last updated:

The strategy design pattern in Java puts each variant of an algorithm, such as a discount rule, a sort order or a payment-routing policy, into its own class behind a common interface. The object that needs the algorithm holds a reference to that interface, so the actual rule can be chosen or swapped at runtime without changing the code that uses it.

It matters because business rules multiply. Marketing adds a new campaign every month, product adds sort options, and finance negotiates new gateway fees. If each rule is another branch in a checkout method, that method grows forever and every change risks breaking the others. Strategy is a behavioral design pattern, and Java developers use it constantly, often without naming it, every time they pass a Comparator.

1. What Is the Strategy Design Pattern?

The GoF intent is: "Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it."

The pattern splits the code into two parts. The context is the class that does a job, for example checkout. One step of that job can be done in several ways, so the context does not implement that step itself; it delegates to a strategy object through an interface. Each concrete strategy implements one way of doing the step.

The strategy is usually passed in through the constructor or a setter, or looked up by name from configuration. Swapping it changes the context's behaviour without editing the context. This is composition over inheritance in its clearest form, and it follows the Open/Closed Principle (new rules are new classes) and the Dependency Inversion Principle (the context depends on an abstraction, not on concrete rules). In modern Java, a strategy interface with one method is a functional interface, so short strategies can be lambdas.

2. Key Components

ComponentResponsibilityIn the Basic Example
StrategyInterface shared by all algorithm variantsDiscountStrategy
Concrete StrategiesOne algorithm eachNoDiscount, PercentageOff, FlatOff, TieredDiscount
ContextHolds a strategy and calls it when the step is neededCheckoutService
ClientChooses the strategy and gives it to the contextmain

The client picks a strategy, often from configuration or the request, and hands it to the context. When the context reaches the variable step, it calls the strategy's method and uses the result. The context never checks which strategy it holds.

3. When Is Strategy Useful?

  • Campaign rules: percentage, flat and tiered discounts that marketing switches on and off.
  • User-selected behaviour: search results sorted by price, rating or recency, depending on a request parameter.
  • Per-tenant policies: each merchant chooses how payments are routed between gateways.

4. UML Class Diagram

The diagram shows the Basic Example.

Strategy UML class diagram The Client creates a discount strategy and passes it to CheckoutService. CheckoutService holds one DiscountStrategy. NoDiscount, PercentageOff, FlatOff and TieredDiscount implement DiscountStrategy. Client uses context CheckoutService - discount: DiscountStrategy + setDiscount(strategy) + total(subtotal): BigDecimal delegates 1 «interface» DiscountStrategy + apply(subtotal): BigDecimal + describe(): String implements NoDiscount PercentageOff FlatOff TieredDiscount «create», then setDiscount()
Strategy: CheckoutService holds one DiscountStrategy and delegates to it, so the client can swap the discount rule at runtime.
  1. CheckoutService (context) holds exactly one DiscountStrategy (solid arrow, multiplicity 1).
  2. The four concrete strategies implement the interface (dashed lines, hollow triangle).
  3. The context has no arrows to any concrete strategy: it cannot depend on a specific rule.
  4. The client creates a concrete strategy and passes it in with setDiscount().

5. Step-by-Step Execution Flow

  1. The client creates a CheckoutService; its default strategy is NoDiscount.
  2. The client creates new PercentageOff(10) and calls setDiscount().
  3. The client calls checkout.total(6400.00).
  4. The context calls discount.apply(6400.00); dynamic dispatch runs PercentageOff.apply(), which returns 5760.00.
  5. The context returns that total. Later, the client sets a TieredDiscount, and the same total() call now applies the tiered rule.

6. Real-World Use Cases

  • E-commerce: discount, shipping-fee, tax and search-ranking rules.
  • Payments and banking: gateway routing, fee calculation, interest calculation per product, and fraud-scoring models.
  • Notifications: retry and back-off policies per channel.
  • The JDK: Comparator is a sorting strategy passed to List.sort() and Collections.sort(). ThreadPoolExecutor takes a RejectedExecutionHandler such as AbortPolicy (the default), CallerRunsPolicy, DiscardPolicy or DiscardOldestPolicy.
  • Spring: Spring Security's PasswordEncoder has interchangeable implementations such as BCryptPasswordEncoder and Argon2PasswordEncoder. Spring also lets you inject a Map<String, YourStrategy> of all strategy beans and pick one by name at runtime.

7. Problem Without the Pattern

A checkout method with every campaign as a branch:

CheckoutWithoutStrategy.java
import java.math.BigDecimal;
import java.math.RoundingMode;

public class CheckoutWithoutStrategy {

    // Every campaign type is another branch inside the checkout
    static BigDecimal total(BigDecimal subtotal, String campaign) {
        if (campaign.equals("NONE")) {
            return subtotal;
        } else if (campaign.equals("DIWALI_10")) {
            return subtotal.multiply(new BigDecimal("0.90")).setScale(2, RoundingMode.HALF_UP);
        } else if (campaign.equals("FLAT_500")) {
            return subtotal.compareTo(new BigDecimal("2000")) >= 0 ? subtotal.subtract(new BigDecimal("500")) : subtotal;
        } else if (campaign.equals("BULK")) {
            // tier rules copied from the marketing spec; the cart page has its own copy
            if (subtotal.compareTo(new BigDecimal("10000")) >= 0) return subtotal.multiply(new BigDecimal("0.85")).setScale(2, RoundingMode.HALF_UP);
            if (subtotal.compareTo(new BigDecimal("5000")) >= 0) return subtotal.multiply(new BigDecimal("0.92")).setScale(2, RoundingMode.HALF_UP);
            return subtotal;
        }
        throw new IllegalArgumentException("Unknown campaign " + campaign);
    }

    public static void main(String[] args) {
        BigDecimal subtotal = new BigDecimal("6400.00");
        for (String campaign : new String[] {"NONE", "DIWALI_10", "FLAT_500", "BULK"}) {
            System.out.println(campaign + ": Rs. " + total(subtotal, campaign));
        }
    }
}
Console
NONE: Rs. 6400.00
DIWALI_10: Rs. 5760.00
FLAT_500: Rs. 5900.00
BULK: Rs. 5888.00
  • An ever-growing method: each campaign adds a branch, and every change re-tests all of them.
  • Duplication: the cart page keeps its own copy of the tier rules, which will drift.
  • String-typed choices: a typo in a campaign name is only caught at runtime.
  • Hard to test: each rule can only be tested through the whole checkout.

8. Solution Using the Pattern

Define a DiscountStrategy interface, write one small class per campaign, and give the checkout a strategy field. Checkout calls discount.apply(subtotal) and nothing else.

The results are identical, but the structure is not. A new campaign is one new class, and checkout never changes. The cart page reuses the same strategy objects instead of copying rules. Each strategy has its own unit tests, and campaigns can be switched at runtime, for example from a feature flag.

9. Strategy Design Pattern in Java: Code Examples

Each example is a single file that runs on Java 21 with java FileName.java.

Basic Example: Checkout Discount Campaigns

Problem: fix section 7 so that each campaign is its own class and checkout never changes when campaigns do.

DiscountStrategyDemo.java
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;

public class DiscountStrategyDemo {

    // Strategy
    interface DiscountStrategy {
        BigDecimal apply(BigDecimal subtotal);
        String describe();
    }

    // Concrete strategies: one rule each
    static final class NoDiscount implements DiscountStrategy {
        public BigDecimal apply(BigDecimal subtotal) { return subtotal; }
        public String describe() { return "no discount"; }
    }

    record PercentageOff(int percent) implements DiscountStrategy {
        public BigDecimal apply(BigDecimal subtotal) {
            return subtotal.multiply(BigDecimal.valueOf(100 - percent)).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
        }
        public String describe() { return percent + "% off"; }
    }

    record FlatOff(BigDecimal amount, BigDecimal minimumSpend) implements DiscountStrategy {
        public BigDecimal apply(BigDecimal subtotal) {
            return subtotal.compareTo(minimumSpend) >= 0 ? subtotal.subtract(amount) : subtotal;
        }
        public String describe() { return "Rs. " + amount + " off above Rs. " + minimumSpend; }
    }

    static final class TieredDiscount implements DiscountStrategy {
        public BigDecimal apply(BigDecimal subtotal) {
            int percent = subtotal.compareTo(new BigDecimal("10000")) >= 0 ? 15
                        : subtotal.compareTo(new BigDecimal("5000")) >= 0 ? 8 : 0;
            return new PercentageOff(percent).apply(subtotal);      // strategies can reuse each other
        }
        public String describe() { return "8% above Rs. 5000, 15% above Rs. 10000"; }
    }

    // Context: uses whatever strategy it is given
    static final class CheckoutService {
        private DiscountStrategy discount = new NoDiscount();

        void setDiscount(DiscountStrategy discount) { this.discount = discount; }

        BigDecimal total(BigDecimal subtotal) { return discount.apply(subtotal); }

        String activeCampaign() { return discount.describe(); }
    }

    public static void main(String[] args) {
        CheckoutService checkout = new CheckoutService();
        BigDecimal subtotal = new BigDecimal("6400.00");

        List<DiscountStrategy> campaigns = List.of(
                new NoDiscount(),
                new PercentageOff(10),
                new FlatOff(new BigDecimal("500"), new BigDecimal("2000")),
                new TieredDiscount());

        for (DiscountStrategy campaign : campaigns) {
            checkout.setDiscount(campaign);                          // swap the algorithm at runtime
            System.out.println(checkout.activeCampaign() + ": Rs. " + checkout.total(subtotal));
        }
    }
}

Important classes and methods

  • PercentageOff and FlatOff are records: their settings are part of the strategy and cannot change after creation.
  • TieredDiscount reuses PercentageOff internally, showing that strategies can build on each other.
  • CheckoutService starts with a safe default, NoDiscount, so it never has a null strategy.

Expected output

Console
no discount: Rs. 6400.00
10% off: Rs. 5760.00
Rs. 500 off above Rs. 2000: Rs. 5900.00
8% above Rs. 5000, 15% above Rs. 10000: Rs. 5888.00

How it runs

  1. The loop swaps the strategy four times on the same checkout object.
  2. Each call to total() gives the same answers as the old if-else version.
  3. 6400.00 falls in the 8% tier, giving 5888.00.

10. Best Practices and Common Mistakes

Best practices

  • Keep the strategy interface small, ideally one method, so lambdas can implement it.
  • Give the context a sensible default strategy instead of allowing null.
  • Register strategies by name (a map or Spring beans) when the choice comes from configuration.
  • Thread safety: shared strategies must be stateless or immutable. If a strategy needs mutable data, keep it in thread-safe structures or create one instance per request.

Common mistakes

  • Moving the if-else elsewhere: choosing strategies with a big switch in every caller instead of one registry.
  • Context type checks such as instanceof PercentageOff, which defeat the abstraction.
  • Fat interfaces that force every strategy to implement methods it does not need.
  • Overengineering: a strategy hierarchy for one rule that has never changed.

11. When to Use and When Not to Use

Use it when the same step has several variants that change independently of the surrounding logic, when the variant is chosen by data such as configuration, user input or tenant, or when each variant deserves its own tests.

Avoid it when there is only one algorithm and no realistic second, when the choice is fixed at compile time and the variants are trivial, or when the variants differ in many related steps, where Template Method or separate classes fit better.

12. Advantages and Disadvantages

Advantages

  • No conditional sprawl in the context.
  • Runtime swapping of behaviour.
  • Open/Closed: new algorithms are new classes or lambdas.
  • Isolated testing of each algorithm.

Disadvantages

  • More types to navigate, unless lambdas are used.
  • The client must know which strategies exist to choose one.
  • A common interface may force all strategies to accept data some do not need.

13. Comparison with Similar Patterns

AspectStrategyStateTemplate MethodCommand
VariesOne algorithm, as a wholeBehaviour by lifecycle stateSome steps of a fixed algorithmThe request itself
MechanismCompositionCompositionInheritanceObjects holding a receiver
Who switchesThe clientThe states themselvesNobody: fixed by the subclassNot applicable
Typical Java formComparator, lambdasEnum or state classesAbstract class with final methodRunnable, command objects

Choose Strategy when a caller picks how one step is done. Choose State when the object switches its own behaviour as its lifecycle advances, Template Method when the overall outline is fixed and a few steps vary by subclass, and Command when you need to store, queue or undo requests. A strategy is often created by a factory that maps names to implementations.

14. Summary

  • The strategy design pattern in Java wraps interchangeable algorithms behind one interface.
  • Purpose: let the algorithm vary independently from the code that uses it.
  • Components: Strategy, Concrete Strategies, Context, Client.
  • The context delegates one step to its current strategy; the client chooses or swaps it.
  • Used for discounts, sorting, payment routing, rejection policies and password encoding.
  • Benefits: no conditional sprawl, runtime flexibility and testable rules.
  • Keep strategies small and stateless, provide a default, and use lambdas for simple cases.

15. Java Design Pattern Interview Questions and Answers

Q1. How do you select a strategy at runtime in a Spring Boot application?

Make each strategy a bean implementing the same interface and inject a Map<String, Strategy>. Spring fills it with every implementation, keyed by bean name, so the request or configuration value selects one directly. Validate unknown keys and fall back to a default.

@Service
class PricingService {
    private final Map<String, DiscountStrategy> strategies;   // e.g. "percentageOff" -> bean

    PricingService(Map<String, DiscountStrategy> strategies) { this.strategies = strategies; }

    BigDecimal price(String campaign, BigDecimal subtotal) {
        return strategies.getOrDefault(campaign, strategies.get("noDiscount")).apply(subtotal);
    }
}
Q2. When should a strategy be a lambda and when a full class?

Use a lambda or method reference for short, stateless rules, like most comparators. Use a class or record when the strategy has configuration (such as a percentage), several helper methods, its own dependencies, or needs a meaningful name in logs and tests.

Q3. How is Strategy different from Template Method?

Strategy uses composition: the context holds an object, and the whole algorithm can be swapped at runtime. Template Method uses inheritance: the base class fixes the outline, and a subclass fills in steps at compile time. If you need to change behaviour per request or tenant, Strategy fits; if the outline must be enforced and only a few steps vary, Template Method fits.

Q4. A shared strategy keeps a running counter in a field, and the numbers come out wrong under load. Why?

One strategy instance is shared by all request threads, so an unsynchronised field such as count++ loses updates. Make strategies stateless; if they need shared counters, use AtomicInteger or LongAdder, or keep per-request state in the call's parameters.

Q5. Different strategies need different inputs: one needs the cart, another the customer's tier. How do you design the interface?

Pass one context object, such as a PricingContext record holding the cart, the customer and the date, so every strategy reads what it needs and the method signature stays stable. Avoid adding parameters for each new strategy, which forces changes on all of them.