Strategy Design Pattern: Clear and Concise with Java Examples

The Strategy pattern lets you define a family of algorithms, encapsulate each one, and make them interchangeable. It enables selecting an algorithm at runtime, promoting flexibility and adherence to SOLID principles.

Understanding the Class Diagram

  • Context: Has a Strategy object and uses it to delegate the work, without knowing the implementation details.

  • Strategy: An interface defining a method (here called execute()), which each concrete strategy implements with its own behavior.

  • ConcreteStrategyA / B: Specific implementations of the strategy, interchangeable at runtime.

How it works: The Context relies on the Strategy to perform the task. This allows swapping behaviors easily, promoting flexibility and following the Open/Closed Principle.

Simple Example

Let’s say we’re implementing a payment system, and we have multiple payment methods:

class PaymentService {
    void pay(String method, int amount) {
        if ("credit".equals(method)) {
            // logic for credit card
        } else if ("paypal".equals(method)) {
            // logic for PayPal
        }
        // more conditions to come...
    }
}

Having a long payment method with the needed logic to process multiple payment methods is rigid, grows unwieldy, and violates Open/Closed Principle (OCP). Each new method increases cyclomatic complexity and requires modifying the existing code.

Applying the Strategy Pattern

interface PaymentStrategy {
    void pay(int amount);
}

class CreditCardPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid " + amount + " with credit card");
    }
}

class PayPalPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid " + amount + " with PayPal");
    }
}

class PaymentService {
    private PaymentStrategy strategy;

    public PaymentService(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void process(int amount) {
        strategy.pay(amount);
    }
}

✅ This avoids conditionals, respects OCP, and makes each strategy testable and reusable — aligning with Single Responsibility Principle (SRP) too.

Example Usage

PaymentService service = new PaymentService(new CreditCardPayment());
service.processPayment(100);  // Outputs: Paid $100 with Credit Card.

service.setStrategy(new PayPalPayment());
service.processPayment(75);   // Outputs: Paid $75 with PayPal.

A Quick Note on Cyclomatic Complexity

Cyclomatic complexity increases as your code has more if-else branches, making it harder to test, understand, and change safely.

❌ Without Strategy Pattern (Multiple Paths)

javaCopyEditclass PaymentService {
    void pay(String method, int amount) {
        if (method.equals("credit")) {
            System.out.println("Paid with Credit Card");
        } else if (method.equals("paypal")) {
            System.out.println("Paid with PayPal");
        } else if (method.equals("applepay")) {
            System.out.println("Paid with Apple Pay");
        }
    }
}

Every new method adds a new branch — more paths, more chances to break something.

✅ With Strategy Pattern (Single Path)

javaCopyEditclass PaymentService {
    private PaymentStrategy strategy;

    public PaymentService(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void process(int amount) {
        strategy.pay(amount); // Always a single, clean path
    }
}

No matter how many strategies you add, process() always follows one path.
This makes your code easier to test, reason about, and extend — which helps maintain quality as the system grows.

Functional Programming: A Modern Alternative?

In languages that support first-class functions, you can simplify the strategy pattern using lambdas:

Consumer<Integer> creditCard = amount -> System.out.println("Paid " + amount + " with credit card");
Consumer<Integer> paypal = amount -> System.out.println("Paid " + amount + " with PayPal");

void processPayment(Consumer<Integer> strategy, int amount) {
    strategy.accept(amount);
}

For short, stateless behaviors, lambdas are concise and flexible. But if the strategy:

  • Has internal state

  • Needs validation logic

  • Requires encapsulation

…then creating a full class (like CreditCardPayment) may be more appropriate to uphold SRP and encapsulation.


When Should You Use the Strategy Pattern?

Look for these clues:

1. You have multiple if-else or switch statements selecting behavior

2. You expect the behavior to change at runtime

Strategy lets you swap logic dynamically — for example, letting a user choose a payment method at runtime.

3. You want to follow the Open/Closed Principle

New behaviors can be added via new strategy classes, without touching existing logic.

4. You want to reuse or test behaviors independently

Each strategy is a standalone unit that’s easier to test and reuse in other parts of the system.

💡 Tip: Let the Strategy Pattern Emerge

Start with the simplest solution. If you notice repeated conditionals or growing complexity, that’s your cue to refactor using the Strategy pattern.

With experience, you’ll start recognizing these patterns earlier and apply them upfront when it makes sense.

🧩 Conclusion

The Strategy pattern is one of the simplest and most practical ways to apply the SOLID principles. In fact, if you study SOLID first, you’ll see that Strategy is a natural implementation of those ideas in action.
👉 You can refresh those principles in my article: SOLID Principles Refresher

By applying this pattern, you get code that is:

  • Easier to maintain

  • Less prone to errors

  • Simpler to test and extend

I think there’s no real downside to using Strategy, as long as you apply it where flexibility is needed and don’t over-engineer parts of your code that are meant to stay simple or static.

👉 Enjoyed this article?
Check out the rest of my Design Patterns series here:
🔗 code-like-a-woman.hashnode.dev/series/design-patterns

0
Subscribe to my newsletter

Read articles from Mirna De Jesus Cambero directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mirna De Jesus Cambero
Mirna De Jesus Cambero

I’m a backend software engineer with over a decade of experience primarily in Java. I started this blog to share what I’ve learned in a simplified, approachable way — and to add value for fellow developers. Though I’m an introvert, I’ve chosen to put myself out there to encourage more women to explore and thrive in tech. I believe that by sharing what we know, we learn twice as much — that’s precisely why I’m here.