Skip to content
Back EndJava

The Ultimate Masterclass: Strategy Pattern in Java 25 & Jakarta EE 11

6 min readsalahtobok
Hero image for Strategy Pattern in Java

The Strategy Pattern is one of the most powerful object-oriented design patterns. As software projects grow, you often find yourself dealing with massive, unreadable blocks of if-else or switch statements to handle different business rules. The Strategy Pattern solves this problem elegantly by encapsulating these rules into separate, interchangeable classes.

In this masterclass, we will explore the Strategy Pattern from its classic Gang of Four (GoF) definition to modern implementations. We will use the latest features of Java 25 and see how to inject strategies dynamically using Jakarta EE 11 and Spring Boot 4.0.0.

What is the Strategy Pattern?

The Strategy Pattern is a behavioral design pattern. It allows you to define a family of algorithms, put each of them into a separate class, and make their objects interchangeable. In simple terms, it lets you switch the “how” (the algorithm) without changing the “what” (the context that uses the algorithm).

This pattern strongly supports the Open/Closed Principle (SOLID). Your application remains open for extension (you can add new strategies) but closed for modification (you do not need to change the core logic).

UML Class Diagram of the Strategy Pattern
The classic structure: A Context uses a Strategy interface, which is implemented by concrete strategies.

The Problem: “If-Else” Hell

Imagine you are building an e-commerce platform. Initially, you only accept Credit Cards. Your PaymentService is simple. Then, the business requests PayPal. Then, Crypto. Soon, your code looks like a nightmare:


public class LegacyPaymentProcessor {
    public void processPayment(String type, double amount) {
        if (type.equals("CREDIT_CARD")) {
            // 50 lines of Credit Card logic
        } else if (type.equals("PAYPAL")) {
            // 50 lines of PayPal logic
        } else if (type.equals("CRYPTO")) {
            // 50 lines of Crypto logic
        } else {
            throw new IllegalArgumentException("Unknown type");
        }
    }
}

This approach violates the Single Responsibility Principle. Every time a new payment method is added, you risk breaking existing logic. If you are learning about structuring applications effectively, you should also read our guide on DDD Layers and Code Structure to keep your domain clean.

Visual metaphor of messy if-else code versus a clean strategy pipeline
Uncontrolled branching creates fragile, tightly-coupled code.

The Modern Java 25 Approach

Let’s rebuild this payment system using the Strategy Pattern with modern Java features. Instead of heavy objects, we will use Java Records to model our data.


// A clean Java Record for our data
public record PaymentRequest(
    BigDecimal amount, 
    PaymentType type, 
    String customerId
) {}

// The Strategy Interface
public interface PaymentStrategy {
    boolean supports(PaymentType type);
    PaymentResult process(PaymentRequest request);
}

The supports() method is crucial. Instead of hardcoding which strategy to use, we ask the strategy itself if it can handle the request. This is sometimes called the Routing Strategy or Registry Pattern.

Implementing Concrete Strategies

Now, we create isolated classes for each payment method. Notice how we use the Jakarta EE @Named annotation to register them as beans in the IoC container.


import jakarta.inject.Named;
import java.util.logging.Logger;

@Named("creditCardStrategy")
public class CreditCardPaymentStrategy implements PaymentStrategy {
    private static final Logger log = Logger.getLogger(CreditCardPaymentStrategy.class.getName());

    @Override
    public boolean supports(PaymentType type) {
        return type == PaymentType.CREDIT_CARD;
    }

    @Override
    public PaymentResult process(PaymentRequest request) {
        log.info("Processing Credit Card for $" + request.amount());
        return new PaymentResult(true, "TXN-CC-123", "Success");
    }
}

Dynamic Injection with Jakarta EE and Spring

The true power of the Strategy Pattern in enterprise applications comes from Dependency Injection. In standard Jakarta CDI, you would use Instance<PaymentStrategy> to inject all available strategies. In Spring Boot 4.0.0, we achieve this by simply injecting a List<PaymentStrategy>.

Flowchart showing a List of strategies injected into a Context class
The framework automatically collects all beans implementing the interface and provides them as a list.

Here is how our Context (the PaymentProcessor) looks:


import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;

@Named
public class PaymentProcessor {

    private final List<PaymentStrategy> strategies;

    @Inject
    public PaymentProcessor(List<PaymentStrategy> strategies) {
        // Spring / CDI injects all discovered strategies here
        this.strategies = strategies;
    }

    public PaymentResult processPayment(PaymentRequest request) {
        return strategies.stream()
                .filter(strategy -> strategy.supports(request.type()))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Unsupported type"))
                .process(request);
    }
}

This design is highly decoupled. If we want to add a CryptoPaymentStrategy tomorrow, we simply create a new class implementing PaymentStrategy and annotate it with @Named. The PaymentProcessor does not change at all. This perfectly aligns with modern architectures, much like the concepts discussed in our Hexagonal Architecture Guide.

Real-World Use Cases

The Strategy Pattern is everywhere in enterprise software. Common examples include:

  • Data Exporting: Generating reports in PDF, CSV, or Excel formats based on user selection.
  • Sorting Algorithms: Switching between QuickSort and MergeSort depending on the size of the dataset.
  • Authentication Methods: Handling OAuth2, JWT, or Basic Auth dynamically.
High level software architecture showing API Gateway routing requests to strategies
In a microservices architecture, strategies often wrap different external API calls.

Advantages and Limitations

Advantages

  • Maintainability: It is much easier to read and test small, isolated strategy classes than a massive service class.
  • Extensibility: New algorithms can be added without modifying existing code.
  • Testability: You can mock individual strategies easily during unit testing.

Limitations

  • Class Explosion: If you have 50 different rules, you will create 50 classes. Sometimes a simple lambda or Enum is better.
  • Context Awareness: The client or the context must understand the difference between strategies to provide the right data.

Best Practices and Common Mistakes

Do not overuse it: For very simple conditional logic, sticking to an if-else or a Java switch expression is perfectly fine. The Strategy Pattern is for complex, changing behaviors.

Keep strategies stateless: A strategy should not hold state (like an instance variable that changes per request). If it does, you will encounter concurrency bugs in multi-threaded environments, especially when dealing with Java 25 Virtual Threads.

Leverage Functional Interfaces: For very lightweight logic, you can implement the Strategy Pattern using Java’s built-in Function or Predicate interfaces instead of creating custom classes.

Conclusion

The Strategy Pattern is essential for any serious Java developer. By combining the classic GoF principles with modern Java 25 features and Jakarta EE dependency injection, you can build systems that are robust, testable, and ready for future changes. Whenever you see a growing switch statement managing business logic, ask yourself if a Strategy would be a cleaner solution.

Companion Workshop Code

You can find the complete, runnable Spring Boot 4.0.0 and Java 25 project for this article on our GitHub repository. It includes the full implementation, REST controllers, and integration tests using MockMvc.

👉 View the source code on GitHub

References

FAQ

Is the Strategy Pattern the same as the State Pattern?

No. While they look identical in UML, their intent is different. A Strategy changes the “how” (the algorithm) but does not change the state of the context. The State Pattern changes the “what” (the behavior) based on internal state transitions.

Can I use Java Enums for the Strategy Pattern?

Yes! If your strategies are simple and do not require heavy dependency injection, using an Enum with an abstract method is a very fast and memory-efficient way to implement the pattern in Java.

Does Spring Boot automatically find my strategies?

Yes. If you annotate your strategy classes with @Component, @Service, or the Jakarta EE @Named annotation, Spring will automatically discover them. If you inject a List<PaymentStrategy>, Spring will fill that list with all the discovered beans.

Comments