Strategy Pattern in Java: Stop Hardcoding Behaviour and Start Swapping It
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.
// ❌ 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 } }
Applying 1.5% transaction fee
Redirecting to PayPal gateway for $49.5
Applying 2.9% PayPal fee
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.
// ✅ 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); } }
[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.
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.
// 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)); } }
Loyal customer price: $40.50
Newsletter subscriber: $40.00
Regular customer price: $45.00
| Aspect | Strategy Pattern (Composition) | Inheritance / Subclassing |
|---|---|---|
| Behaviour swap timing | Runtime — swap strategies on the fly | Compile time — fixed in the class hierarchy |
| Adding new behaviour | Write a new Strategy class, zero existing changes | Add a new subclass, may touch base class |
| Open/Closed Principle | ✅ Fully respected | ❌ Often violated when base class changes |
| Testability | Each strategy tested in total isolation | Subclass tests drag in base class dependencies |
| Multiple behaviours | Compose multiple strategies on one context | Limited by single inheritance in Java |
| Best fit | Algorithm family that varies independently | Shared structure with minor specialisation |
| Java 8+ shorthand | Lambda for single-method strategies | Not applicable |
| Complexity cost | Slight increase in class count | Deep 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.
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.