Implementing Strategy Pattern in Spring Boot to Replace Conditional Logic

First, define the strategy interface and its various implementations:

// Strategy contract for payment processing
public interface PaymentProcessor {
    TransactionResult processTransaction(PaymentRequest request);
}

// Credit card payment implementation
@Service
public class CreditCardPaymentProcessor implements PaymentProcessor {
    @Override
    public TransactionResult processTransaction(PaymentRequest request) {
        // Apply credit card processing logic
        double fee = request.getAmount() * 0.029; // 2.9% processing fee
        return new TransactionResult(request.getAmount() - fee, "SUCCESS", "Credit card processed");
    }
}

// PayPal payment implementation
@Service
public class PayPalPaymentProcessor implements PaymentProcessor {
    @Override
    public TransactionResult processTransaction(PaymentRequest request) {
        // Apply PayPal processing logic
        double fee = request.getAmount() * 0.032; // 3.2% processing fee
        return new TransactionResult(request.getAmount() - fee, "SUCCESS", "PayPal processed");
    }
}

// Bank transfer payment implementation
@Service
public class BankTransferPaymentProcessor implements PaymentProcessor {
    @Override
    public TransactionResult processTransaction(PaymentRequest request) {
        // Apply bank transfer logic
        double fee = Math.max(5.0, request.getAmount() * 0.001); // Minimum $5 fee
        return new TransactionResult(request.getAmount() - fee, "SUCCESS", "Bank transfer completed");
    }
}

Next, create a context class that manages and selects the appropriate strategy:

@Service
public class PaymentContext {
    
    private final Map<PaymentMethod, PaymentProcessor> processorMap;
    
    public PaymentContext(List<PaymentProcessor> processors) {
        this.processorMap = processors.stream()
            .collect(Collectors.toMap(
                this::determinePaymentMethod,
                Function.identity()
            ));
    }
    
    public TransactionResult executePayment(PaymentRequest request) {
        PaymentProcessor processor = processorMap.get(request.getPaymentMethod());
        if (processor == null) {
            throw new UnsupportedPaymentMethodException("Payment method not supported: " + request.getPaymentMethod());
        }
        return processor.processTransaction(request);
    }
    
    private PaymentMethod determinePaymentMethod(PaymentProcessor processor) {
        if (processor instanceof CreditCardPaymentProcessor) {
            return PaymentMethod.CREDIT_CARD;
        } else if (processor instanceof PayPalPaymentProcessor) {
            return PaymentMethod.PAYPAL;
        } else if (processor instanceof BankTransferPaymentProcessor) {
            return PaymentMethod.BANK_TRANSFER;
        }
        throw new IllegalStateException("Unknown payment processor type");
    }
}

Now, let's define the supporting model classes:

public enum PaymentMethod {
    CREDIT_CARD, PAYPAL, BANK_TRANSFER
}

public class PaymentRequest {
    private String accountNumber;
    private double amount;
    private PaymentMethod paymentMethod;
    private Currency currency;
    
    // Constructors, getters, and setters
}

public class TransactionResult {
    private final double netAmount;
    private final String status;
    private final String message;
    
    // Constructor and getters
}

Finally, here's how to use the payment context in a REST controller:

@RestController
@RequestMapping("/api/payments")
public class PaymentController {
    
    private final PaymentContext paymentContext;
    
    public PaymentController(PaymentContext paymentContext) {
        this.paymentContext = paymentContext;
    }
    
    @PostMapping("/process")
    public ResponseEntity<TransactionResult> processPayment(@RequestBody PaymentRequest request) {
        try {
            TransactionResult result = paymentContext.executePayment(request);
            return ResponseEntity.ok(result);
        } catch (UnsupportedPaymentMethodException e) {
            return ResponseEntity.badRequest()
                .body(new TransactionResult(0, "FAILED", e.getMessage()));
        }
    }
}

This approach provides several benefits over traditional cnoditional logic:

  • New payment methods can be added without modifying existing code (Open/Closed Principal)
  • Each payment processing logic is encapsulated in its own class
  • Testing becomes simpler as each strategy can be tested independently
  • Spring's dependency injection automatically manages strategy registration

For even more dynamic strategy selection, you can use Spring's conditional bean registration:

@Configuration
public class PaymentConfig {
    
    @Bean
    @ConditionalOnProperty(name = "payment.creditcard.enabled", havingValue = "true")
    public PaymentProcessor creditCardProcessor() {
        return new CreditCardPaymentProcessor();
    }
    
    @Bean
    @ConditionalOnProperty(name = "payment.paypal.enabled", havingValue = "true")
    public PaymentProcessor payPalProcessor() {
        return new PayPalPaymentProcessor();
    }
}

Tags: Spring Boot Strategy Pattern Dependency Injection java Design Patterns

Posted on Tue, 19 May 2026 18:15:36 +0000 by ace01