Strategy Pattern explained simply
Thilan Dissanayaka Software Architecture February 06, 2020

Strategy Pattern explained simply

Ever written a method that is basically a giant if-else chain deciding which algorithm or behavior to use? You add one more option, and the whole thing gets longer and uglier. The Strategy Pattern exists to kill that mess for good.

What is the Strategy Pattern?

  • Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
  • Lets the algorithm vary independently from the clients that use it.
  • Replaces conditional logic with composition – you inject the behavior you want instead of hard-coding it.

In short:

Same goal, different approaches. Pick one at runtime.

Real-Life Analogy

Open Google Maps and search for a destination. You will see options: driving, walking, cycling, public transit. The destination stays the same, but the strategy to get there changes completely. Each mode calculates a different route, a different ETA, and different directions.

You didn’t rewrite the entire navigation system for each mode. You just swapped the routing strategy. That is exactly what the Strategy Pattern does in code.

Structure

  • Context (CheckoutService): Holds a reference to a strategy object. It delegates the actual work to the strategy instead of implementing it directly.
  • Strategy (PaymentStrategy): An interface that all concrete strategies implement. Defines the contract – in our case, a single pay() method.
  • Concrete Strategies (CreditCardPayment, PayPalPayment, CryptoPayment): Each one encapsulates a specific algorithm or behavior. They are interchangeable because they share the same interface.

The key insight here is that the CheckoutService never needs to know how a payment is processed. It just calls pay() on whatever strategy it has been given. New payment method? Write a new class, plug it in. Done.

Example in Java – A Payment Processing System

This is a classic use case. Your checkout flow needs to support multiple payment methods, and you want to add new ones without touching existing code.

PaymentStrategy Interface

public interface PaymentStrategy {
    boolean pay(double amount);
}

One method. That is all the context needs to know about.

CreditCardPayment

public class CreditCardPayment implements PaymentStrategy {
    private final String cardNumber;
    private final String cardHolder;

    public CreditCardPayment(String cardNumber, String cardHolder) {
        this.cardNumber = cardNumber;
        this.cardHolder = cardHolder;
    }

    @Override
    public boolean pay(double amount) {
        if (cardNumber == null || cardNumber.length() < 13) {
            System.out.println("[CREDIT CARD] Invalid card number.");
            return false;
        }
        // simulate charge processing
        String masked = "****-" + cardNumber.substring(cardNumber.length() - 4);
        System.out.println("[CREDIT CARD] Charged $" + amount + " to card " + masked + " (" + cardHolder + ")");
        return true;
    }
}

Validates the card, masks the number, processes the charge. Everything specific to credit card processing lives here and nowhere else.

PayPalPayment

public class PayPalPayment implements PaymentStrategy {
    private final String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public boolean pay(double amount) {
        System.out.println("[PAYPAL] Authenticating account: " + email);
        // simulate PayPal API authentication
        if (email == null || !email.contains("@")) {
            System.out.println("[PAYPAL] Authentication failed.");
            return false;
        }
        System.out.println("[PAYPAL] Transferred $" + amount + " from " + email);
        return true;
    }
}

Different flow entirely – authenticate first, then transfer. But from the outside, the interface is identical.

CryptoPayment

public class CryptoPayment implements PaymentStrategy {
    private final String walletAddress;

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

    @Override
    public boolean pay(double amount) {
        if (walletAddress == null || walletAddress.length() < 26) {
            System.out.println("[CRYPTO] Invalid wallet address.");
            return false;
        }
        String shortAddr = walletAddress.substring(0, 6) + "..." + walletAddress.substring(walletAddress.length() - 4);
        System.out.println("[CRYPTO] Sending $" + amount + " in crypto to wallet " + shortAddr);
        return true;
    }
}

Validates the wallet address, truncates it for display, sends the transaction. Completely different internals, same interface.

CheckoutService (The Context)

public class CheckoutService {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public boolean checkout(double amount) {
        if (paymentStrategy == null) {
            System.out.println("No payment method selected.");
            return false;
        }
        System.out.println("Processing checkout for $" + amount + "...");
        boolean success = paymentStrategy.pay(amount);
        if (success) {
            System.out.println("Payment successful. Order confirmed.\n");
        } else {
            System.out.println("Payment failed. Please try again.\n");
        }
        return success;
    }
}

Notice what is not here. No if-else chain. No switch statement. No mention of credit cards, PayPal, or crypto. The CheckoutService just delegates to whatever strategy is set. That is the entire point.

Putting It All Together

import java.util.Map;

public class MainProgram {
    public static void main(String[] args) {
        CheckoutService checkout = new CheckoutService();

        // simulate user selecting payment methods dynamically
        Map<String, PaymentStrategy> strategies = Map.of(
            "credit_card", new CreditCardPayment("4539148803436467", "Alice Johnson"),
            "paypal", new PayPalPayment("[email protected]"),
            "crypto", new CryptoPayment("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
        );

        // user picks credit card
        String userChoice = "credit_card";
        checkout.setPaymentStrategy(strategies.get(userChoice));
        checkout.checkout(49.99);

        // same user switches to PayPal for next purchase
        userChoice = "paypal";
        checkout.setPaymentStrategy(strategies.get(userChoice));
        checkout.checkout(129.00);

        // and crypto for another
        userChoice = "crypto";
        checkout.setPaymentStrategy(strategies.get(userChoice));
        checkout.checkout(0.05);
    }
}

Output:

Processing checkout for $49.99...
[CREDIT CARD] Charged $49.99 to card ****-6467 (Alice Johnson)
Payment successful. Order confirmed.

Processing checkout for $129.0...
[PAYPAL] Authenticating account: [email protected]
[PAYPAL] Transferred $129.0 from [email protected]
Payment successful. Order confirmed.

Processing checkout for $0.05...
[CRYPTO] Sending $0.05 in crypto to wallet 1A1zP1...fNa
Payment successful. Order confirmed.

The user switches payment methods at runtime. The CheckoutService does not change at all. No conditionals, no casting, no type checking. Just polymorphism doing its job.

Key Components

Component Role In Our Example
Strategy Interface defining the algorithm contract PaymentStrategy
Concrete Strategy A specific implementation of the algorithm CreditCardPayment, PayPalPayment, CryptoPayment
Context The class that uses a strategy CheckoutService

When to Use the Strategy Pattern

  • You have multiple algorithms for the same task and want to switch between them at runtime.
  • A class has a massive conditional block that selects different behavior based on type.
  • You want to isolate algorithm-specific code from the business logic that uses it.
  • You need to add new behaviors without modifying existing classes (Open/Closed Principle).

Advantages

  • Eliminates conditionals: No more growing if-else or switch blocks.
  • Open/Closed Principle: Add new strategies without touching existing code.
  • Runtime flexibility: Swap algorithms on the fly based on configuration, user input, or system state.
  • Testability: Each strategy can be unit tested in isolation.
  • Single Responsibility: Each strategy class has one job.

Disadvantages

  • More classes: Every new algorithm means a new class. For very simple cases, this can feel like overkill.
  • Client awareness: The calling code needs to know which strategies exist and when to use each one. The decision logic moves somewhere – it doesn’t disappear entirely.
  • Communication overhead: If strategies need shared state or data from the context, passing that data around can get awkward.

Real-World Use Cases

  • Java Comparator: Collections.sort(list, comparator) – the comparator is the strategy. Swap it to sort by name, date, price, whatever you want.
  • Spring Security AuthenticationProvider: Each provider (LDAP, database, OAuth) is a strategy for authenticating users. Spring tries them in order until one succeeds.
  • Compression algorithms: A file archiver might support ZIP, GZIP, and BZIP2. Each is a compression strategy. The archiver just calls compress() on whichever one the user picked.
  • Sorting strategies: A data pipeline might use quicksort for large datasets and insertion sort for small ones. Same interface, different strategies selected based on input size.
  • Logging frameworks: Log to console, file, or remote server. Same log call, different output strategies.

Strategy vs Template Method Pattern

Both patterns deal with varying algorithms, so they get confused often. Here is the difference.

The Template Method Pattern defines the skeleton of an algorithm in a base class and lets subclasses override specific steps. The overall flow is fixed – only the details change. It uses inheritance.

The Strategy Pattern encapsulates entire algorithms as separate objects and lets you swap them at runtime. It uses composition.

In practical terms: if the algorithm structure is the same but certain steps vary, use Template Method. If the entire algorithm changes, use Strategy.

Think of it this way – Template Method says “follow these steps, but customize step 3.” Strategy says “here are three completely different ways to do this. Pick one.”

Wrapping Up

The Strategy Pattern is one of the most practical design patterns you will use. Anytime you catch yourself writing a conditional chain to pick between different algorithms or behaviors, stop and ask: can I extract these into strategy objects instead?

The payment processing example shows the real power – adding a new payment method like Apple Pay means writing one new class that implements PaymentStrategy. You don’t touch CheckoutService. You don’t touch any existing payment class. You just plug it in and it works.

That is clean, extensible design. No if-else required.

ALSO READ
Writing a Shell Code for Linux
Apr 21, 2020 Exploit Development

Shellcode is a small piece of machine code used as the payload in exploit development. In this post, we write Linux shellcode from scratch — starting with a simple exit, building up to spawning a shell, and explaining every decision along the way.

Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Common Web Application Attacks
Feb 05, 2020 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.

> > >