Strategy Design Pattern in Java: Real-World Payment Example

Strategy Design Pattern in Java: Real-World Payment Example

Strategy Design Pattern in Java: Complete Guide with a Real-World Payment Example

Imagine you are building a Spring Boot payment service. Today, the application supports UPI payments. Next month, the business asks you to support debit cards, credit cards, and net banking. A few months later, another payment provider needs to be integrated.

Your first implementation might use a large if-else or switch statement inside the payment service. That works initially, but every new payment method adds another branch to code that already handles important business operations.

There is a cleaner way to design this functionality. The Strategy Design Pattern lets you define a common contract for different payment algorithms and keep each implementation separate. Your main service can focus on processing the request rather than knowing the internal details of every payment method.

In this guide, we will build a payment strategy example in plain Java, understand the design with an original SVG diagram, and then see how to apply the same idea in Spring Boot.

1. What Is the Strategy Design Pattern in Java?

The Strategy Design Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each algorithm behind a common interface, and allows them to be used interchangeably.

Instead of writing every possible way to perform a task inside one class, we create separate implementations for the different ways of doing it. The calling code uses the common interface and does not need to know how each implementation works internally.

For example, a payment application might support UPI, credit or debit cards, and net banking. All three can expose the same operation, such as pay(), while implementing it differently.

The main goal: make algorithms interchangeable without coupling the main business service to every implementation.

When should you consider using it?

  • Several implementations perform the same general task differently.
  • New algorithms are likely to be introduced over time.
  • A service contains many conditional branches for selecting behavior.
  • You want to test each algorithm independently.
  • You want to follow the Open/Closed Principle where practical.

2. Real-World Example: Payment Processing

Consider a checkout service. The customer chooses a payment method, and the application processes the payment through the appropriate implementation.

The checkout service should not need to know the internal steps of UPI, card, or net banking processing. It should work with a common payment interface.

Payment Method Strategy Class Responsibility
UPI UpiPaymentStrategy Initiate payment using a UPI integration.
Credit or debit card CardPaymentStrategy Initiate a card payment through an approved provider.
Net banking NetBankingStrategy Initiate payment using the bank payment flow.

The example in this article uses simulated payment operations so you can run it locally without credentials or a payment gateway. These implementations demonstrate the design pattern, not actual financial transaction processing.

3. The Problem with if-else Statements

A first implementation might look like this:

class PaymentService {

    public void pay(String method, double amount) {

        if ("UPI".equalsIgnoreCase(method)) {
            System.out.println("Processing UPI payment: " + amount);

        } else if ("CARD".equalsIgnoreCase(method)) {
            System.out.println("Processing card payment: " + amount);

        } else if ("NET_BANKING".equalsIgnoreCase(method)) {
            System.out.println("Processing net banking payment: " + amount);

        } else {
            throw new IllegalArgumentException(
                "Unsupported payment method: " + method
            );
        }
    }
}

This code is easy to understand initially. The difficulty appears when every payment method needs different validation, provider integration, error handling, and testing.

As the number of methods increases, the service starts to own responsibilities that belong to the individual payment implementations.

  • The payment service becomes harder to maintain.
  • Adding a new payment option requires editing existing selection logic.
  • Testing one payment implementation may require testing a large service.
  • Provider-specific details can leak into the main business service.

There is nothing inherently wrong with if-else. For a few simple branches that are unlikely to change, it may be the best choice. Strategy becomes valuable when the algorithms have meaningful behavior of their own or need to evolve independently.

4. How the Strategy Pattern Works

1. Strategy interface

Defines the common operation that all strategies must implement. Our example uses PaymentStrategy with a pay() method.

2. Concrete strategies

Each implementation contains the behavior for a particular payment method. For example, UpiPaymentStrategy and CardPaymentStrategy implement the same interface in different ways.

3. Context

The context uses the selected strategy. In our example, PaymentProcessor delegates payment execution to a PaymentStrategy instead of implementing all payment algorithms itself.

The strategy can be provided when the context is constructed or selected through a registry or controlled selection mechanism. The key idea is that the processing code depends on the interface, not every concrete implementation.

5. Strategy Design Pattern Diagram

This original SVG shows how the payment processor delegates execution to the selected strategy.

Strategy Design Pattern for Java payment processing Checkout calls PaymentProcessor, which uses the PaymentStrategy interface. The interface has three implementations: UPI, Card, and Net Banking strategies. Checkout Client PaymentProcessor Context calls PaymentStrategy Interface delegates UpiPaymentStrategy Concrete strategy CardPaymentStrategy Concrete strategy NetBankingStrategy Concrete strategy One contract, multiple interchangeable implementations
Figure: The payment processor uses the common interface and delegates to the selected concrete strategy.

6. Complete Java Implementation of the Strategy Pattern

Let's implement the example using Java 17. To make it easy to run, all classes are nested in one file. In a production codebase, you would normally separate these types into individual files.

Step 1: Create the PaymentStrategy interface

interface PaymentStrategy {
    String pay(double amount);
}

Step 2: Implement UPI payment

final class UpiPaymentStrategy implements PaymentStrategy {

    @Override
    public String pay(double amount) {
        return "UPI payment initiated for INR " + amount;
    }
}

Step 3: Implement card payment

final class CardPaymentStrategy implements PaymentStrategy {

    @Override
    public String pay(double amount) {
        return "Card payment initiated for INR " + amount;
    }
}

Step 4: Implement net banking

final class NetBankingStrategy implements PaymentStrategy {

    @Override
    public String pay(double amount) {
        return "Net banking payment initiated for INR " + amount;
    }
}

Step 5: Create the PaymentProcessor context

final class PaymentProcessor {

    private final PaymentStrategy strategy;

    PaymentProcessor(PaymentStrategy strategy) {
        this.strategy = java.util.Objects.requireNonNull(strategy);
    }

    public String processPayment(double amount) {
        if (!Double.isFinite(amount) || amount <= 0) {
            throw new IllegalArgumentException(
                "Amount must be a positive finite number"
            );
        }

        return strategy.pay(amount);
    }
}

This sample validates that the amount is positive and finite. For real financial calculations, use BigDecimal with an explicit currency and rounding policy instead of double.

Step 6: Complete runnable example

Save this code as StrategyPatternDemo.java.

import java.util.Objects;

public class StrategyPatternDemo {

    interface PaymentStrategy {
        String pay(double amount);
    }

    static final class UpiPaymentStrategy implements PaymentStrategy {
        @Override
        public String pay(double amount) {
            return "UPI payment initiated for INR " + amount;
        }
    }

    static final class CardPaymentStrategy implements PaymentStrategy {
        @Override
        public String pay(double amount) {
            return "Card payment initiated for INR " + amount;
        }
    }

    static final class NetBankingStrategy implements PaymentStrategy {
        @Override
        public String pay(double amount) {
            return "Net banking payment initiated for INR " + amount;
        }
    }

    static final class PaymentProcessor {
        private final PaymentStrategy strategy;

        PaymentProcessor(PaymentStrategy strategy) {
            this.strategy = Objects.requireNonNull(strategy);
        }

        public String processPayment(double amount) {
            if (!Double.isFinite(amount) || amount <= 0) {
                throw new IllegalArgumentException(
                    "Amount must be a positive finite number"
                );
            }

            return strategy.pay(amount);
        }
    }

    public static void main(String[] args) {

        PaymentProcessor upiProcessor =
            new PaymentProcessor(new UpiPaymentStrategy());

        PaymentProcessor cardProcessor =
            new PaymentProcessor(new CardPaymentStrategy());

        PaymentProcessor netBankingProcessor =
            new PaymentProcessor(new NetBankingStrategy());

        System.out.println(upiProcessor.processPayment(1500));
        System.out.println(cardProcessor.processPayment(2500));
        System.out.println(netBankingProcessor.processPayment(3500));
    }
}

7. How to Run the Java Example

  1. Install a JDK, such as Java 17.
  2. Create StrategyPatternDemo.java.
  3. Paste the complete code from the previous section.
  4. Open a terminal in that directory.
  5. Compile and run the program.
javac StrategyPatternDemo.java
java StrategyPatternDemo

Expected output

UPI payment initiated for INR 1500.0
Card payment initiated for INR 2500.0
Net banking payment initiated for INR 3500.0

The same context class works with all three implementations. To introduce another payment strategy, implement PaymentStrategy and supply the new implementation to the processor.

8. Strategy Design Pattern with Spring Boot

In Spring Boot, you can register each strategy as a bean and inject the implementations into a service. This is useful when different providers or algorithms need to be maintained independently.

Step 1: Define the interface

public interface PaymentStrategy {
    String paymentType();
    PaymentResult pay(PaymentRequest request);
}

Step 2: Define request and result types

import java.math.BigDecimal;

public record PaymentRequest(
    String paymentReference,
    BigDecimal amount,
    String currency
) {}

public record PaymentResult(
    String paymentReference,
    String status,
    String message
) {}

Step 3: Create a UPI strategy bean

import org.springframework.stereotype.Component;

@Component
public class UpiPaymentStrategy implements PaymentStrategy {

    @Override
    public String paymentType() {
        return "UPI";
    }

    @Override
    public PaymentResult pay(PaymentRequest request) {
        // Integrate with an approved UPI provider here.
        return new PaymentResult(
            request.paymentReference(),
            "PENDING",
            "UPI request prepared for provider integration"
        );
    }
}

Step 4: Add a card strategy

import org.springframework.stereotype.Component;

@Component
public class CardPaymentStrategy implements PaymentStrategy {

    @Override
    public String paymentType() {
        return "CARD";
    }

    @Override
    public PaymentResult pay(PaymentRequest request) {
        // Integrate with the card processor's secure SDK/API.
        return new PaymentResult(
            request.paymentReference(),
            "PENDING",
            "Card request prepared for provider integration"
        );
    }
}

Step 5: Create a strategy registry

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final Map<String, PaymentStrategy> strategies;

    public PaymentService(List<PaymentStrategy> strategyList) {
        this.strategies = strategyList.stream()
            .collect(Collectors.toUnmodifiableMap(
                strategy -> strategy.paymentType()
                    .toUpperCase(java.util.Locale.ROOT),
                Function.identity()
            ));
    }

    public PaymentResult pay(
        String paymentType,
        PaymentRequest request
    ) {
        if (paymentType == null || paymentType.isBlank()) {
            throw new IllegalArgumentException(
                "Payment type is required"
            );
        }

        PaymentStrategy strategy = strategies.get(
            paymentType.toUpperCase(java.util.Locale.ROOT)
        );

        if (strategy == null) {
            throw new IllegalArgumentException(
                "Unsupported payment type: " + paymentType
            );
        }

        return strategy.pay(request);
    }
}

The registry detects duplicate normalized payment types during initialization. The service contains selection logic, but no longer contains a separate branch for every concrete payment algorithm.

These Spring Boot snippets are integration examples, not a complete payment gateway. Actual payment processing requires provider APIs, authentication, error handling, and transaction management.

9. Selecting a Strategy Dynamically

In plain Java, the caller selects a strategy by constructing a processor with the desired implementation. In Spring Boot, selection may come from a validated request field, server-side configuration, routing rules, or a provider selection service.

// Illustrative controller method body
public PaymentResult initiatePayment(
    String paymentType,
    PaymentRequest request
) {
    return paymentService.pay(paymentType, request);
}

The controller should not implement UPI or card processing itself. It delegates to the service, which selects a registered strategy.

For production systems, do not let an untrusted client bypass risk controls or select arbitrary internal provider names. Validate which payment methods are permitted for the merchant, transaction, currency, and customer.

10. Production Considerations for Payment Systems

Use BigDecimal for monetary amounts

Represent money with BigDecimal and an explicit currency. Define scale and rounding rules according to the currency and provider requirements.

Use idempotency for retries

Network failures and client retries can submit the same request more than once. Use a persistent idempotency key or transaction reference and enforce uniqueness where appropriate.

Distinguish initiation from confirmation

A payment request being accepted does not mean funds have been successfully captured. Model pending, successful, failed, and reversed outcomes explicitly, and verify provider callbacks or status responses.

Protect sensitive information

Do not log card security codes, full card credentials, authentication secrets, or sensitive payment tokens. Follow the payment provider's approved integration and security requirements.

Handle provider failures

Timeouts, rate limits, and provider outages need appropriate timeouts, bounded retries, circuit breakers, and reconciliation. A retry must not accidentally create a duplicate payment.

Separate selection from routing policy

The Strategy pattern provides interchangeable behavior. Rules such as provider health, merchant configuration, supported currency, cost, or risk policy may belong in a separate routing component that chooses a strategy.

11. Strategy Pattern in the Java JDK

You do not need a payment application to see the Strategy pattern in Java. The JDK provides useful examples of the same idea.

Example 1: Comparator

The Comparator interface lets a caller choose how objects should be ordered. The same list can be sorted by natural or reverse order without rewriting the sorting algorithm.

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

List<String> names = new ArrayList<>(
    List.of("Ravi", "Aman", "Deep")
);

names.sort(Comparator.naturalOrder());
System.out.println(names);

names.sort(Comparator.reverseOrder());
System.out.println(names);

Example 2: Custom sorting with a lambda

List<String> products = new ArrayList<>(
    List.of("Laptop", "Mouse", "Keyboard")
);

products.sort(Comparator.comparingInt(String::length));
System.out.println(products);

The comparator defines a different ordering rule. Lambdas are useful when a strategy is small enough to express as a function rather than a separate class.

12. Strategy vs State vs Factory Pattern

Pattern Main Purpose Example
Strategy Makes interchangeable algorithms available through a common contract. Select UPI, card, or net banking processing.
State Changes behavior according to an object's current state and lifecycle. Allow or reject order operations based on order status.
Factory Encapsulates object creation or selection. Create or retrieve the appropriate payment strategy.

These patterns can work together. A factory or registry may select a strategy, while the strategy performs the selected algorithm. A state-based workflow might also use a strategy for a particular operation.

13. Advantages and Disadvantages

Advantages

  • Loose coupling: the context depends on an interface rather than concrete algorithms.
  • Extensibility: new implementations can often be introduced without changing the context.
  • Testability: each algorithm can be tested independently.
  • Clear responsibilities: each strategy owns one variation of the behavior.
  • Composition over inheritance: behavior can be supplied as an object.

Disadvantages

  • More classes: a separate class for every trivial algorithm may be unnecessary.
  • Selection still matters: the application needs a way to choose the correct strategy.
  • Additional abstraction: interfaces and registries can complicate a simple operation if overused.
  • Shared responsibilities remain: common validation, authorization, logging, and transaction handling still need a sensible design.

Choose the pattern when the benefits of isolating the algorithms outweigh the extra structure. Good design is about making change easier, not eliminating every conditional statement.

14. Real-World Use Cases

  • Payment processing: different payment methods or provider integrations.
  • Shipping calculation: standard, express, or international shipping rates.
  • Discount calculation: coupon-based, seasonal, membership, or promotional discounts.
  • File compression: different compression algorithms.
  • Authentication: interchangeable authentication mechanisms, with security policy enforced separately.
  • Sorting: different ordering rules through Java's Comparator.
  • Notification delivery: alternative channels such as email or SMS.
  • Route planning: shortest, fastest, or other route-selection algorithms.

The common feature is that the application needs to perform a task in multiple ways, and those implementations can be selected independently of the code that uses them.

15. Strategy Design Pattern Interview Questions and Answers

Q1. What is the Strategy Design Pattern?

Answer: It is a behavioral design pattern that encapsulates a family of algorithms behind a common interface and makes them interchangeable.

Q2. What problem does it solve?

Answer: It separates algorithm implementations from the code that uses them, helping avoid large conditional blocks when different algorithms need to be selected.

Q3. What are the main components?

Answer: A strategy interface, concrete strategy implementations, and a context that delegates to the selected strategy.

Q4. How does Strategy support the Open/Closed Principle?

Answer: New algorithms can often be introduced as new implementations without modifying the existing context. Selection or configuration code may still need changes depending on the architecture.

Q5. Can we implement Strategy using Java 8 lambdas?

Answer: Yes. A functional interface with one abstract method can be implemented with a lambda when the algorithm is small and does not need its own class.

Q6. What is the difference between Strategy and Factory?

Answer: Strategy represents interchangeable behavior. Factory focuses on creating or selecting objects. A factory can be used to obtain a strategy implementation.

Q7. How would you implement Strategy in Spring Boot?

Answer: Define an interface, register each implementation as a Spring bean, inject the implementations into a service, and select the appropriate one through a validated registry or routing policy.

Q8. Does Strategy make a payment application production-ready?

Answer: No. It structures the code. A real payment integration also needs authentication, authorization, secure provider integration, idempotency, error handling, persistence, and reconciliation.

Q9. When should we not use Strategy?

Answer: Avoid adding the pattern when a small and stable conditional is clearer than several separate classes.

Q10. Can a strategy be changed at runtime?

Answer: Yes, if the context accepts a new strategy or resolves one for each operation. The exact design depends on whether selection is per request, per object, or based on configuration.

16. Frequently Asked Questions

What is the Strategy Design Pattern in Java with a real-world example?

The Strategy pattern lets a Java application choose between interchangeable algorithms. A payment service that supports UPI, cards, and net banking is a practical example.

Is Strategy a behavioral design pattern?

Yes. It is a behavioral pattern because it organizes how an application delegates and selects behavior.

Can Strategy be used with Spring Boot and microservices?

Yes. Spring dependency injection can supply multiple implementations of a strategy interface. Each microservice should still have clear ownership of its business logic and external integrations.

Does Strategy completely remove if-else statements?

No. It moves algorithm-specific behavior out of the context. Some selection logic may still be needed in a registry or routing component.

What is a common JDK example of Strategy?

Comparator is a useful example. A sorting operation can use different comparison implementations without changing the sorting algorithm.

17. Strategy Pattern Implementation Checklist

  • Identify the algorithms that vary.
  • Define a small, clear strategy interface.
  • Implement each algorithm independently.
  • Keep the context focused on orchestration.
  • Choose strategies through explicit, validated selection logic.
  • Test each strategy independently and test the selection mechanism.
  • Keep shared validation and authorization in appropriate layers.
  • For payment applications, handle idempotency, provider errors, and transaction outcomes separately.
  • Avoid introducing extra classes for trivial, stable conditions.

Conclusion

The Strategy Design Pattern is a practical way to keep Java applications flexible when the same task can be performed using different algorithms. In our payment example, UPI, card, and net banking implementations follow a common interface, allowing the payment processor to delegate work without knowing each implementation's details.

In Spring Boot, dependency injection and a strategy registry can make it easier to add implementations while keeping business services focused. The same approach is useful for discounts, shipping calculations, routing algorithms, and other areas where behavior varies.

Remember that patterns are tools, not rules. Start with the simplest design that meets your needs, and introduce Strategy when separating the algorithms makes the code easier to change, test, and understand.

No comments: