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();
}
}