Home Java Strategy Pattern in Java: Stop Hardcoding Behaviour and Start Swapping It

Strategy Pattern in Java: Stop Hardcoding Behaviour and Start Swapping It

In Plain English 🔥
Imagine you're navigating to a coffee shop. You can walk, bike, or take the bus — the destination is the same, but the method of getting there changes. Your phone's maps app doesn't rewrite itself for each transport type; it just swaps in a different 'travel strategy' at runtime. That's exactly what the Strategy Pattern does in code: it lets you swap out the algorithm (the how) without touching the logic that uses it (the what).
⚡ Quick Answer
Imagine you're navigating to a coffee shop. You can walk, bike, or take the bus — the destination is the same, but the method of getting there changes. Your phone's maps app doesn't rewrite itself for each transport type; it just swaps in a different 'travel strategy' at runtime. That's exactly what the Strategy Pattern does in code: it lets you swap out the algorithm (the how) without touching the logic that uses it (the what).

Every real application eventually hits this wall: a class that needs to do something slightly differently depending on the situation. Maybe it's a payment system that needs to handle credit cards, PayPal, and crypto. Maybe it's a sorting engine that chooses between quicksort and mergesort based on data size. The naive fix is a chain of if-else or switch statements inside the class itself. That works — until the requirements change, which they always do. Now you're cracking open a class that was already tested and trusted, and every edit is a potential regression.

The Strategy Pattern is a behavioural design pattern from the Gang of Four that fixes exactly this problem. Instead of stuffing multiple algorithms into one bloated class, you extract each algorithm into its own small, focused class behind a common interface. The original class just holds a reference to whichever strategy it currently needs and delegates the work to it. Adding a new algorithm means writing a new class — nothing already working gets touched.

By the end of this article you'll understand why the Strategy Pattern exists at a design level, how to implement it cleanly in Java with a realistic payment-processing example, how it compares to just using plain inheritance, and the two mistakes that trip up almost every developer the first time they reach for this pattern. You'll also walk away with sharp answers for the interview questions that always come up around it.

The Problem Strategy Solves — Why if-else Doesn't Scale

Let's build the problem before we solve it. You're writing a checkout system. Initially you only support credit cards, so you write the charge logic right inside your OrderProcessor class. Three months later, PayPal lands on the roadmap. You add an if block. Then crypto arrives. Then buy-now-pay-later. Before long your processPayment method is 150 lines of conditional logic, and every new payment type requires a developer to read, understand, and carefully not break the existing branches.

This violates the Open/Closed Principle — one of the SOLID principles — which says a class should be open for extension but closed for modification. Every time you add a payment method, you're modifying OrderProcessor. That class is now fragile: a bug introduced for crypto can accidentally break credit card processing.

The pain is real: testing becomes harder because you can't test each algorithm in isolation, the class grows without bound, and onboarding a new developer means handing them a wall of conditionals with no clear seams. The Strategy Pattern gives you those seams.

NaiveOrderProcessor.java · JAVA
1234567891011121314151617181920212223242526272829303132333435
// ❌ THE ANTI-PATTERN — what NOT to do
// Every new payment method forces us to edit this class.
// One typo here can break ALL payment methods at once.
public class NaiveOrderProcessor {

    // The payment type is passed as a raw string — brittle and error-prone
    public void processPayment(String paymentType, double amount) {

        if (paymentType.equals("CREDIT_CARD")) {
            // Credit card processing logic
            System.out.println("Charging $" + amount + " to credit card via Stripe API");
            System.out.println("Applying 1.5% transaction fee");

        } else if (paymentType.equals("PAYPAL")) {
            // PayPal processing logic — completely different flow
            System.out.println("Redirecting to PayPal gateway for $" + amount);
            System.out.println("Applying 2.9% PayPal fee");

        } else if (paymentType.equals("CRYPTO")) {
            // Crypto logic — yet another completely different flow
            System.out.println("Opening crypto wallet for $" + amount + " in BTC");
            System.out.println("Waiting for blockchain confirmation...");

        } else {
            throw new IllegalArgumentException("Unknown payment type: " + paymentType);
        }
    }

    public static void main(String[] args) {
        NaiveOrderProcessor processor = new NaiveOrderProcessor();
        processor.processPayment("CREDIT_CARD", 99.99);
        processor.processPayment("PAYPAL", 49.50);
        // Adding a new payment type means coming back HERE and adding another else-if
    }
}
▶ Output
Charging $99.99 to credit card via Stripe API
Applying 1.5% transaction fee
Redirecting to PayPal gateway for $49.5
Applying 2.9% PayPal fee
⚠️
Watch Out:If your class has a method with more than two or three if-else branches selecting between different algorithms, that's a strong signal the Strategy Pattern belongs here. The test is simple: can each branch be described as 'a way of doing X'? If yes, extract them.

Building the Strategy Pattern from Scratch — A Clean Payment System

The Strategy Pattern has three moving parts: the Strategy interface (the contract every algorithm must honour), the Concrete Strategies (the actual algorithm implementations), and the Context (the class that uses a strategy without caring which one it is).

The Context holds a reference to a PaymentStrategy interface — not to any specific implementation. This is the critical move. Because the Context talks to the interface, you can hand it any concrete strategy at runtime and it just works. This is dependency injection in its simplest, most elegant form.

Notice what you gain immediately: each payment class is small, focused, and independently testable. You can write a unit test for CryptoPaymentStrategy without ever touching OrderProcessor. Adding a new payment method means writing one new class and wiring it in — zero modifications to existing, tested code. That's the Open/Closed Principle in action.

StrategyPatternPayment.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
// ✅ THE STRATEGY PATTERN — clean, extensible, and testable

// ── STEP 1: Define the Strategy interface ──────────────────────────────────
// Every payment method MUST implement this contract.
// The Context (OrderProcessor) only ever talks to this interface.
interface PaymentStrategy {
    void pay(double amount);          // the algorithm hook
    double calculateFee(double amount); // bonus: fees are also encapsulated per strategy
}

// ── STEP 2: Concrete Strategy A — Credit Card ──────────────────────────────
class CreditCardPaymentStrategy implements PaymentStrategy {

    private final String cardHolderName;
    private final String maskedCardNumber; // store only last 4 digits for safety

    public CreditCardPaymentStrategy(String cardHolderName, String maskedCardNumber) {
        this.cardHolderName = cardHolderName;
        this.maskedCardNumber = maskedCardNumber;
    }

    @Override
    public void pay(double amount) {
        double fee = calculateFee(amount);
        double totalCharged = amount + fee;
        // This class ONLY knows about credit card logic — nothing else
        System.out.printf("[Credit Card] Charging $%.2f (+ $%.2f fee = $%.2f total) "
                + "to card ending in %s for %s%n",
                amount, fee, totalCharged, maskedCardNumber, cardHolderName);
    }

    @Override
    public double calculateFee(double amount) {
        return amount * 0.015; // Stripe charges 1.5%
    }
}

// ── STEP 3: Concrete Strategy B — PayPal ───────────────────────────────────
class PayPalPaymentStrategy implements PaymentStrategy {

    private final String paypalEmail;

    public PayPalPaymentStrategy(String paypalEmail) {
        this.paypalEmail = paypalEmail;
    }

    @Override
    public void pay(double amount) {
        double fee = calculateFee(amount);
        double totalCharged = amount + fee;
        System.out.printf("[PayPal] Redirecting %s to PayPal for $%.2f "
                + "(+ $%.2f PayPal fee = $%.2f total)%n",
                paypalEmail, amount, fee, totalCharged);
    }

    @Override
    public double calculateFee(double amount) {
        return amount * 0.029; // PayPal charges 2.9%
    }
}

// ── STEP 4: Concrete Strategy C — Crypto ───────────────────────────────────
class CryptoPaymentStrategy implements PaymentStrategy {

    private final String walletAddress;

    public CryptoPaymentStrategy(String walletAddress) {
        this.walletAddress = walletAddress;
    }

    @Override
    public void pay(double amount) {
        double fee = calculateFee(amount);
        // Crypto has network fees but we pass savings on to the user
        System.out.printf("[Crypto] Sending $%.2f (network fee: $%.2f) "
                + "to wallet %s — awaiting blockchain confirmation%n",
                amount, fee, walletAddress);
    }

    @Override
    public double calculateFee(double amount) {
        return 0.50; // flat $0.50 network fee regardless of amount
    }
}

// ── STEP 5: The Context — OrderProcessor ───────────────────────────────────
// This class has NO idea which payment method is being used.
// It just calls pay() and the strategy handles everything.
class OrderProcessor {

    // Holds a reference to the interface — NOT a concrete class
    private PaymentStrategy paymentStrategy;

    // Strategy is injected via the constructor (constructor injection)
    public OrderProcessor(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    // You can also swap the strategy at runtime — powerful for multi-step checkouts
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(double orderTotal) {
        System.out.println("\n=== Processing Order: $" + orderTotal + " ===");
        paymentStrategy.pay(orderTotal); // delegate — don't care HOW it's done
        System.out.println("Order confirmed. Receipt sent.");
    }
}

// ── STEP 6: Wire it all together ───────────────────────────────────────────
public class StrategyPatternPayment {

    public static void main(String[] args) {

        // Customer A pays by credit card
        PaymentStrategy creditCard = new CreditCardPaymentStrategy("Alice Johnson", "4242");
        OrderProcessor aliceCheckout = new OrderProcessor(creditCard);
        aliceCheckout.checkout(120.00);

        // Customer B pays via PayPal
        PaymentStrategy paypal = new PayPalPaymentStrategy("bob@example.com");
        OrderProcessor bobCheckout = new OrderProcessor(paypal);
        bobCheckout.checkout(75.00);

        // Customer C switches from PayPal to crypto mid-session (runtime swap!)
        PaymentStrategy crypto = new CryptoPaymentStrategy("0xABCD1234EF");
        bobCheckout.setPaymentStrategy(crypto); // hot-swap the strategy
        bobCheckout.checkout(75.00);
    }
}
▶ Output
=== Processing Order: $120.0 ===
[Credit Card] Charging $120.00 (+ $1.80 fee = $121.80 total) to card ending in 4242 for Alice Johnson
Order confirmed. Receipt sent.

=== Processing Order: $75.0 ===
[PayPal] Redirecting bob@example.com to PayPal for $75.00 (+ $2.18 PayPal fee = $77.18 total)
Order confirmed. Receipt sent.

=== Processing Order: $75.0 ===
[Crypto] Sending $75.00 (network fee: $0.50) to wallet 0xABCD1234EF — awaiting blockchain confirmation
Order confirmed. Receipt sent.
⚠️
Pro Tip:Notice that `OrderProcessor` never imports or references `CreditCardPaymentStrategy`, `PayPalPaymentStrategy`, or `CryptoPaymentStrategy` directly. It only knows about the `PaymentStrategy` interface. This is the key: you could ship `OrderProcessor` as a compiled library JAR and users could add new payment strategies without touching your code at all.

Strategy vs Inheritance — Why Composition Wins Here

The most common question when first seeing the Strategy Pattern is: 'Why not just subclass OrderProcessor and override the checkout method?' It's a fair question. Inheritance feels natural for this kind of variation.

Here's why it falls apart: with inheritance, each subclass carries the entire OrderProcessor with it. If a customer wants to change payment method mid-session — say they start with PayPal and switch to crypto — you'd need to replace the entire object, not just swap one behaviour. Inheritance bakes behaviour into the class hierarchy at compile time. Strategy swaps it at runtime.

Inheritance also creates a rigid tree. What if a processor needs to combine behaviours — say, a payment strategy AND a discount strategy? Multiple inheritance isn't available in Java, and deep hierarchies become a maintenance nightmare. Composition ('has-a') is almost always more flexible than inheritance ('is-a') when the varying behaviour needs to be independently changeable. The Gang of Four literally coined the phrase 'favour composition over inheritance' — the Strategy Pattern is the canonical example of why.

StrategyWithLambda.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// BONUS: In Java 8+, if your Strategy interface has ONE method,
// you can use lambdas as lightweight strategies — no boilerplate class needed.
// This is perfect for simple, one-off strategies defined at the call site.

@FunctionalInterface // marks this as safe to use with lambdas
interface DiscountStrategy {
    double apply(double originalPrice);
}

class PricingEngine {

    private DiscountStrategy discountStrategy;

    public PricingEngine(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double calculateFinalPrice(double originalPrice) {
        return discountStrategy.apply(originalPrice);
    }
}

public class StrategyWithLambda {

    public static void main(String[] args) {

        // Strategy 1: 10% off for loyal customers — defined inline as a lambda
        PricingEngine loyalCustomerPricing = new PricingEngine(
                price -> price * 0.90  // the lambda IS the strategy
        );

        // Strategy 2: Flat $5 off for newsletter subscribers
        PricingEngine newsletterPricing = new PricingEngine(
                price -> Math.max(0, price - 5.00) // never go below $0
        );

        // Strategy 3: No discount for regular customers
        PricingEngine regularPricing = new PricingEngine(
                price -> price // identity — no change
        );

        double basePrice = 45.00;

        System.out.printf("Base price:               $%.2f%n", basePrice);
        System.out.printf("Loyal customer price:     $%.2f%n", loyalCustomerPricing.calculateFinalPrice(basePrice));
        System.out.printf("Newsletter subscriber:    $%.2f%n", newsletterPricing.calculateFinalPrice(basePrice));
        System.out.printf("Regular customer price:   $%.2f%n", regularPricing.calculateFinalPrice(basePrice));
    }
}
▶ Output
Base price: $45.00
Loyal customer price: $40.50
Newsletter subscriber: $40.00
Regular customer price: $45.00
🔥
Interview Gold:When an interviewer asks 'how is the Strategy Pattern different from using inheritance?', the killer answer is: 'Strategy uses composition to vary behaviour at runtime; inheritance locks behaviour into the class hierarchy at compile time. Strategy also follows the Single Responsibility Principle — each algorithm lives in its own class — whereas inheritance tends to push multiple concerns into one hierarchy.'
AspectStrategy Pattern (Composition)Inheritance / Subclassing
Behaviour swap timingRuntime — swap strategies on the flyCompile time — fixed in the class hierarchy
Adding new behaviourWrite a new Strategy class, zero existing changesAdd a new subclass, may touch base class
Open/Closed Principle✅ Fully respected❌ Often violated when base class changes
TestabilityEach strategy tested in total isolationSubclass tests drag in base class dependencies
Multiple behavioursCompose multiple strategies on one contextLimited by single inheritance in Java
Best fitAlgorithm family that varies independentlyShared structure with minor specialisation
Java 8+ shorthandLambda for single-method strategiesNot applicable
Complexity costSlight increase in class countDeep hierarchies become unreadable fast

🎯 Key Takeaways

  • The Strategy Pattern extracts a family of interchangeable algorithms into separate classes behind a shared interface, letting the Context swap them at runtime without modification — this is the Open/Closed Principle made tangible.
  • The Context should receive its strategy from outside (via constructor or setter injection) and never contain logic to choose between strategies internally — that selection logic belongs in a factory or the calling code.
  • In Java 8+, if your Strategy interface has exactly one abstract method, annotate it with @FunctionalInterface and callers can pass lambdas directly — dramatically reducing boilerplate for simple, one-off strategies.
  • The pattern's real power isn't the first strategy you extract — it's the fourth one you add six months later without touching a single line of already-tested, already-deployed code.

⚠ Common Mistakes to Avoid

  • Mistake 1: Passing raw strings or enums to select the strategy inside the Context — Symptom: you end up with a switch statement inside the Context class, defeating the entire purpose of the pattern, since adding a new strategy still requires modifying the Context. Fix: the strategy selection logic belongs OUTSIDE the Context — in a factory, a configuration file, or the calling code. The Context should receive an already-constructed strategy object and never decide which one to use internally.
  • Mistake 2: Making the Strategy interface too granular or too broad — Symptom: either you have a dozen one-liner strategies that could share a richer interface, or one giant interface forces every strategy to implement five methods it doesn't need (with ugly empty stub implementations). Fix: design the interface around the minimal contract the Context actually needs. If some strategies legitimately don't need certain operations, split into two narrower interfaces. A Strategy interface should almost never have more than two or three methods.
  • Mistake 3: Using the Strategy Pattern when simple polymorphism (plain inheritance) was the right tool — Symptom: you create a Strategy interface with only one concrete implementation and no realistic prospect of a second one, adding indirection with no benefit. Fix: apply the pattern when you have a genuine family of interchangeable algorithms and can name at least two concrete strategies right now. If you're 'preparing for future flexibility' with only one variant, you're over-engineering. Write the simplest thing that works and refactor to Strategy when the second variant actually arrives.

Interview Questions on This Topic

  • QCan you explain the Strategy Pattern and give a real-world example where you'd choose it over a chain of if-else statements? What specific design principles does it honour?
  • QWhat's the difference between the Strategy Pattern and the State Pattern? They look almost identical in structure — how do you decide which one to use?
  • QIf your Strategy interface has only one method, how does Java 8 change the way you'd implement this pattern, and what are the trade-offs of using lambdas versus full concrete classes for strategies?

Frequently Asked Questions

When should I use the Strategy Pattern in Java?

Use it when you have two or more algorithms that do the same job differently and you need to be able to swap between them — either at runtime or as requirements grow. The clearest signal is a class that already contains multiple if-else or switch branches where each branch is a distinct algorithm. If you can name the branches as 'Strategy A', 'Strategy B', and so on, it's time to refactor.

Is the Strategy Pattern the same as dependency injection?

They're closely related but not the same thing. Dependency injection is a broader technique for supplying objects with their dependencies from outside, rather than having them create their own. The Strategy Pattern specifically uses that injection technique to supply interchangeable algorithms. You could say the Strategy Pattern is a specific, intentional application of dependency injection focused on behaviour variation.

Does using the Strategy Pattern mean I'll end up with too many small classes?

Yes — and that's usually a good thing. More small, focused classes is almost always better than fewer large, tangled ones. Each strategy class has a single responsibility, is easy to name, easy to test, and easy to delete if the business requirement it represents is dropped. If the class count genuinely concerns you, Java 8 lambdas let you define trivial strategies inline without a dedicated class.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousObserver Pattern in JavaNext →Decorator Pattern in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged