Skip to content
Spring Boot sb design-patterns 3 min read

Strategy Pattern

The strategy pattern defines a family of interchangeable algorithms behind a common interface so the algorithm can be chosen at runtime. Spring makes this pattern almost effortless: declare an interface, register each implementation as a bean, and inject them as a List<T> or Map<String, T>. The container collects every implementing bean for you, so adding a new strategy is just adding a new class.

The shape of the pattern

A strategy is an interface plus several implementations:

public interface PaymentStrategy {
    boolean supports(PaymentType type);
    PaymentResult pay(PaymentRequest request);
}
import org.springframework.stereotype.Component;

@Component
public class CardPaymentStrategy implements PaymentStrategy {
    public boolean supports(PaymentType type) { return type == PaymentType.CARD; }
    public PaymentResult pay(PaymentRequest r) { /* charge card */ return PaymentResult.ok(); }
}

@Component
public class PaypalPaymentStrategy implements PaymentStrategy {
    public boolean supports(PaymentType type) { return type == PaymentType.PAYPAL; }
    public PaymentResult pay(PaymentRequest r) { /* call PayPal */ return PaymentResult.ok(); }
}

@Component
public class BankTransferStrategy implements PaymentStrategy {
    public boolean supports(PaymentType type) { return type == PaymentType.BANK; }
    public PaymentResult pay(PaymentRequest r) { /* initiate transfer */ return PaymentResult.ok(); }
}

Injecting a List of strategies

When you inject List<PaymentStrategy>, Spring populates it with every bean implementing the interface. A registry then picks the right one at runtime:

import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final List<PaymentStrategy> strategies;   // all impls, injected by Spring

    public PaymentResult process(PaymentRequest request) {
        return strategies.stream()
                .filter(s -> s.supports(request.type()))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(
                        "No strategy for " + request.type()))
                .pay(request);
    }
}

Adding a new payment method — say CryptoPaymentStrategy — means writing one new @Component. The PaymentService never changes. That is the open/closed principle, delivered by the strategy pattern plus DI.

Injecting a Map for direct lookup

Inject Map<String, PaymentStrategy> and Spring keys the map by bean name, giving O(1) selection without iterating:

@Service
public class PaymentService {

    private final Map<String, PaymentStrategy> strategiesByName;

    public PaymentService(Map<String, PaymentStrategy> strategiesByName) {
        this.strategiesByName = strategiesByName;
    }

    public PaymentResult process(String beanName, PaymentRequest request) {
        PaymentStrategy strategy = strategiesByName.get(beanName);
        if (strategy == null) throw new IllegalArgumentException("Unknown: " + beanName);
        return strategy.pay(request);
    }
}

The keys are the bean names (cardPaymentStrategy, paypalPaymentStrategy, …). Use an explicit name — @Component("CARD") — to make the key match your domain enum value directly.

List vs Map injection

ApproachSelectionBest when
List<T> + supports(...)Iterate and ask eachSelection logic is richer than a key
Map<String, T>Look up by bean nameA simple key maps directly to a strategy

Tip: Control iteration order of the injected List with @Order on each strategy (or by implementing Ordered). The first matching strategy wins, so ordering lets you express precedence and fallbacks.

Strategies in the framework itself

Spring is full of strategy interfaces with swappable implementations:

  • PasswordEncoderBCryptPasswordEncoder, Argon2PasswordEncoder, Pbkdf2PasswordEncoder. You pick one as a bean; Spring Security uses it everywhere.
  • HandlerMethodArgumentResolver — each one knows how to resolve a particular controller parameter type (@RequestBody, @PathVariable, Pageable, …). The MVC dispatcher tries each resolver until one supportsParameter.
  • HttpMessageConverter — Jackson, form, string converters chosen by content type.
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();   // swap the strategy in one place
}

Note: The supports(...)/pay(...) split you wrote for PaymentStrategy mirrors exactly how HandlerMethodArgumentResolver works (supportsParameter + resolveArgument). Recognising the pattern in the framework makes its source code far easier to read.

Last updated June 13, 2026
Was this helpful?