DEV Community

Leonardo Pires
Leonardo Pires

Posted on

Applying the Open/Closed Principle with the Strategy Pattern in Spring Boot

How can we apply the Open/Closed Principle using the Strategy Pattern?*

The Open/Closed Principle (OCP) states that software entities should be open for extension but closed for modification.

In practice, this means that we should be able to add new behaviors without constantly changing code that already works.

Imagine a system that supports different payment methods, such as PIX, credit cards, and bank slips.

When all payment rules are concentrated in a single method using multiple if, else, or switch statements, every new payment method requires modifying the existing service.

The Strategy Pattern helps solve this problem by defining a common abstraction and separating each payment behavior into its own implementation.

This allows us to introduce a new payment method by creating a new strategy without changing the service responsible for selecting and executing it.

In this relationship:

Open/Closed is the principle we want to follow.

Strategy is a well-known design pattern that helps us apply it through abstraction, composition, and polymorphism.

Here is a complete example using Java and Spring Boot:

public interface PaymentStrategy {

    String getType();

    void pay(BigDecimal amount);
}
Enter fullscreen mode Exit fullscreen mode

Each payment method implements the same interface:

@Component
public class PixPaymentStrategy implements PaymentStrategy {

    @Override
    public String getType() {
        return "PIX";
    }

    @Override
    public void pay(BigDecimal amount) {
        System.out.println(
            "Processing PIX payment: " + amount
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

@Component
public class CreditCardPaymentStrategy
        implements PaymentStrategy {

    @Override
    public String getType() {
        return "CREDIT_CARD";
    }

    @Override
    public void pay(BigDecimal amount) {
        System.out.println(
            "Processing credit card payment: " + amount
        );
    }
}
Enter fullscreen mode Exit fullscreen mode
@Component
public class BoletoPaymentStrategy
        implements PaymentStrategy {

    @Override
    public String getType() {
        return "BOLETO";
    }

    @Override
    public void pay(BigDecimal amount) {
        System.out.println(
            "Generating bank slip: " + amount
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Spring injects all implementations of PaymentStrategy into the service.

The service then creates a map in which the payment type is the key and the corresponding strategy is the value:

@Service
public class PaymentService {

    private final Map<String, PaymentStrategy> strategies;

    public PaymentService(
            List<PaymentStrategy> strategyList
    ) {
        this.strategies = strategyList.stream()
            .collect(
                Collectors.toMap(
                    strategy -> strategy
                        .getType()
                        .toUpperCase(),
                    Function.identity()
                )
            );
    }

    public void pay(
            String paymentType,
            BigDecimal amount
    ) {
        PaymentStrategy strategy = strategies.get(
            paymentType.toUpperCase()
        );

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

        strategy.pay(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

A custom exception can be used when the requested strategy does not exist:

public class UnsupportedPaymentMethodException
        extends RuntimeException {

    public UnsupportedPaymentMethodException(
            String message
    ) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

The request object receives the payment type and amount:

public record PaymentRequest(
    String paymentType,
    BigDecimal amount
) {
}
Enter fullscreen mode Exit fullscreen mode

Finally, the controller delegates the request to the payment service:

@RestController
@RequestMapping("/payments")
public class PaymentController {

    private final PaymentService paymentService;

    public PaymentController(
            PaymentService paymentService
    ) {
        this.paymentService = paymentService;
    }

    @PostMapping
    public ResponseEntity<Void> pay(
            @RequestBody PaymentRequest request
    ) {
        paymentService.pay(
            request.paymentType(),
            request.amount()
        );

        return ResponseEntity.noContent().build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now imagine that the application needs to support PayPal.

We only need to add a new implementation:

@Component
public class PaypalPaymentStrategy
        implements PaymentStrategy {

    @Override
    public String getType() {
        return "PAYPAL";
    }

    @Override
    public void pay(BigDecimal amount) {
        System.out.println(
            "Processing PayPal payment: " + amount
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The PaymentService does not need to be changed.

Spring automatically discovers the new component and injects it into the list of available strategies.

The system remains:

Open for extension: new payment strategies can be added.

Closed for modification: the service responsible for processing payments remains unchanged.

The Open/Closed Principle defines the design goal.

The Strategy Pattern provides a practical way to achieve it.

Top comments (0)