Factory + Strategy Design Patterns: Payment Processing in Java
Learn how to combine Factory and Strategy patterns to build flexible, maintainable payment processing systems. Real-world example with complete Java implementation.

Factory + Strategy Design Patterns: Building Flexible Payment Systems
Design patterns are reusable solutions to common problems in software design. When used together strategically, they create flexible, maintainable systems that scale with changing requirements.
This guide explores how combining Factory Pattern and Strategy Pattern creates robust payment processing systems that can easily support new payment methods without changing existing code.
Why Combine Factory and Strategy?
Imagine building a payment processing system that supports multiple payment methods: card, cash, digital wallet, and tomorrow—cryptocurrency.
Without patterns:
- You'd have massive if-else chains that grow with each new payment method.
- processPayment() method knows about all payment types, violating the Open/Closed Principle.
// ❌ BAD: Tight coupling and hard to extend
public void processPayment(String method, double amount) {
if ("CARD".equals(method)) {
// card logic
} else if ("CASH".equals(method)) {
// cash logic
} else if ("DIGITAL_WALLET".equals(method)) {
// wallet logic
}
// Each new method requires modifying this code
}With patterns: New payment methods integrate seamlessly without touching existing code.
Understanding the Patterns
Strategy Pattern
What: Encapsulates a family of algorithms and makes them interchangeable.
When to use: When you have multiple ways to perform a task and want to select at runtime.
Benefit: Isolates the algorithm from the client, allowing you to switch strategies easily.
Factory Pattern
What: Creates objects without specifying their exact classes.
When to use: When object creation logic is complex or should be centralized.
Benefit: Decouples object creation from usage, centralizing instantiation logic.
Why Together?
- Strategy defines what algorithms you have
- Factory defines how to create them
- Together they create a system that's both flexible and maintainable
When to choose this over Enum + Lambda
Use Factory + Strategy classes when strategy behavior is more than small formula-style logic.
- Each strategy needs its own collaborators (gateway clients, repositories, metrics, retry handlers)
- You want stronger unit-test isolation per strategy class
- Strategy behavior is likely to grow independently over time
- Strategy selection may become config-driven or environment-specific
Use Enum + Lambda when strategies are small, fixed, and mostly stateless.
In short: start with Enum + Lambda for lightweight rules; move to Factory + Strategy classes when complexity grows.
Architecture Diagram
The flow below shows how OrderService asks the factory for a processor, and how each concrete processor implements the same PaymentProcessor strategy contract.
Step-by-Step Implementation
Let's build a payment processing system from the ground up.
Step 1: Define Payment Methods
public enum PaymentMethod {
CARD, CASH, DIGITAL_WALLET
}Use an enum to represent all available payment methods. This provides type safety and makes it easy to add new methods.
Step 2: Define Payment Status
public enum PaymentStatus {
PENDING, COMPLETED, FAILED
}Track the status of each payment transaction.
Step 3: Create the Strategy Interface
This is the core of our system—the common contract all payment processors must follow:
public interface PaymentProcessor {
/**
* Process a payment of the given amount.
*
* @param amount The payment amount to process
* @return true if successful, false otherwise
*/
boolean processPayment(double amount);
}Key Point: Every payment method implements this single interface, ensuring consistent behavior.
Step 4: Implement Concrete Strategies
Now create specific implementations for each payment method:
public class CardPaymentProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing CARD payment of amount $" + amount);
// In real world: Call payment gateway (Stripe, Square, etc.)
try {
// Simulate gateway processing time
Thread.sleep(1000);
// Simulate validation
if (amount > 0) {
System.out.println("Card payment SUCCESS");
return true;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
}public class CashPaymentProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing CASH payment of amount $" + amount);
// In real world: Verify cash register, update inventory
try {
Thread.sleep(500); // Cash is faster
if (amount > 0) {
System.out.println("Cash payment SUCCESS");
return true;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
}public class DigitalWalletPaymentProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing Digital Wallet payment of amount $" + amount);
// In real world: Call PayPal, Apple Pay, Google Pay API
try {
Thread.sleep(2000); // Wallet can be slower due to API calls
if (amount > 0 && amount < 100000) { // Example validation
System.out.println("Wallet payment SUCCESS");
return true;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
}Observation: Each strategy is independent and handles its own logic. Adding a new payment method only requires creating a new class that implements PaymentProcessor.
Step 5: Create the Factory
The factory centralizes the creation of payment processors:
public class PaymentProcessorFactory {
/**
* Factory method to get the appropriate payment processor
*
* @param method The payment method to use
* @return An instance of the appropriate PaymentProcessor
* @throws IllegalArgumentException if method is not supported
*/
public static PaymentProcessor getProcessor(PaymentMethod method) {
return switch(method) {
case CASH -> new CashPaymentProcessor();
case CARD -> new CardPaymentProcessor();
case DIGITAL_WALLET -> new DigitalWalletPaymentProcessor();
};
}
}Java 17+ Tip: The switch expression (not statement) is cleaner and mandatory for exhaustiveness checking.
Using the System
Now clients use the system without worrying about which payment processor to use:
public class OrderService {
public void checkout(double amount, PaymentMethod paymentMethod) {
try {
// Factory creates the right processor
PaymentProcessor processor = PaymentProcessorFactory.getProcessor(paymentMethod);
// Strategy executes the payment
boolean success = processor.processPayment(amount);
if (success) {
System.out.println("Order completed successfully");
} else {
System.out.println("Payment failed");
}
} catch (IllegalArgumentException e) {
System.out.println("Unsupported payment method: " + e.getMessage());
}
}
}Usage Example
public class Main {
public static void main(String[] args) {
OrderService service = new OrderService();
// Same code, different behavior based on PaymentMethod
service.checkout(99.99, PaymentMethod.CARD); // Calls CardPaymentProcessor
service.checkout(50.00, PaymentMethod.CASH); // Calls CashPaymentProcessor
service.checkout(199.99, PaymentMethod.DIGITAL_WALLET); // Calls DigitalWalletPaymentProcessor
}
}Adding a New Payment Method
This is where the pattern shines. Want to add cryptocurrency support?
Only add two lines to existing code:
public enum PaymentMethod {
CARD, CASH, DIGITAL_WALLET, CRYPTOCURRENCY // ✅ Add here
}
public class PaymentProcessorFactory {
public static PaymentProcessor getProcessor(PaymentMethod method) {
return switch(method) {
case CASH -> new CashPaymentProcessor();
case CARD -> new CardPaymentProcessor();
case DIGITAL_WALLET -> new DigitalWalletPaymentProcessor();
case CRYPTOCURRENCY -> new CryptoPaymentProcessor(); // ✅ Add here
};
}
}Then create the new strategy:
public class CryptoPaymentProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing CRYPTO payment of amount $" + amount);
// Crypto-specific logic
return true;
}
}Zero changes to OrderService, checkout(), or client code!
Best Practices
1. Immutability
Keep payment processors stateless:
✅ GOOD: Stateless processors
public class CardPaymentProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) { /* ... */ }
}
❌ BAD: Storing state
public class CardPaymentProcessor implements PaymentProcessor {
private String transactionId; // Mutable state
// Can cause issues in concurrent environments
}2. Factory Caching
For expensive processor creation, cache instances:
public class PaymentProcessorFactory {
private static final Map<PaymentMethod, PaymentProcessor> cache =
new ConcurrentHashMap<>();
public static PaymentProcessor getProcessor(PaymentMethod method) {
return cache.computeIfAbsent(method, key -> switch(key) {
case CARD -> new CardPaymentProcessor();
case CASH -> new CashPaymentProcessor();
case DIGITAL_WALLET -> new DigitalWalletPaymentProcessor();
});
}
}3. Dependency Injection
In real applications, inject dependencies:
@Component
public class PaymentProcessorFactory {
@Autowired
private CardPaymentService cardService;
@Autowired
private CashRegisterService cashService;
public PaymentProcessor getProcessor(PaymentMethod method) {
return switch(method) {
case CARD -> new CardPaymentProcessor(cardService);
case CASH -> new CashPaymentProcessor(cashService);
case DIGITAL_WALLET -> new DigitalWalletPaymentProcessor();
};
}
}4. Configuration-Driven
Make payment methods configurable:
@Configuration
public class PaymentConfig {
@Value("${app.payment.methods:CARD,CASH,DIGITAL_WALLET}")
private List<String> enabledMethods;
}Common Mistakes to Avoid
❌ Mistake 1: Adding Logic to Factory
// DON'T do this
public class PaymentProcessorFactory {
public static PaymentProcessor getProcessor(PaymentMethod method) {
PaymentProcessor processor = switch(method) {
case CARD -> new CardPaymentProcessor();
// ...
};
// Factory should NOT contain business logic
processor.processPayment(100); // ❌ Wrong place
return processor;
}
}❌ Mistake 2: Exposing Implementation
// DON'T do this
public class OrderService {
public void checkout(double amount) {
CardPaymentProcessor card = new CardPaymentProcessor(); // ❌ Tight coupling
card.processPayment(amount);
}
}❌ Mistake 3: God Factory
// DON'T do this - Factory doing too much
public class PaymentProcessorFactory {
public PaymentProcessor getProcessor(String method,
double amount,
User user,
Order order) { // ❌ Too many params
// Complex logic here
}
}Testing Your Design
Unit Testing Strategies
@Test
public void testCardPaymentProcessing() {
// Create a test double if needed
PaymentProcessor processor = new CardPaymentProcessor();
assertTrue(processor.processPayment(50.0));
assertFalse(processor.processPayment(-10.0));
}
@Test
public void testFactoryCreatesCorrectProcessor() {
PaymentProcessor card = PaymentProcessorFactory.getProcessor(PaymentMethod.CARD);
assertInstanceOf(CardPaymentProcessor.class, card);
PaymentProcessor cash = PaymentProcessorFactory.getProcessor(PaymentMethod.CASH);
assertInstanceOf(CashPaymentProcessor.class, cash);
}
@Test
public void testPaymentPolymorphism() {
// Test that all strategies work consistently
PaymentMethod[] methods = PaymentMethod.values();
for (PaymentMethod method : methods) {
PaymentProcessor processor = PaymentProcessorFactory.getProcessor(method);
// All should handle valid amounts
assertTrue(processor.processPayment(25.0));
}
}When NOT to Use This Pattern
- Simple systems with only one way to do something (use Strategy when 2+ variants exist)
- Performance-critical loops creating many processor instances (add caching/pooling)
- Overkill for trivial logic (balance simplicity vs. extensibility)
Summary
The combination of Factory + Strategy patterns provides:
| Aspect | Benefit |
|---|---|
| Flexibility | Switch between implementations at runtime |
| Maintainability | Each algorithm in its own class |
| Testability | Easy to mock and test strategies independently |
| Scalability | Add new methods without modifying existing code |
| SOLID Principles | Follows Open/Closed, Single Responsibility, Dependency Inversion |
This pattern is ideal for payment processing, logging strategies, authentication methods, compression algorithms, and any system with multiple interchangeable implementations.
Enjoyed this article?
Check out more articles or share this with your network.