Loading learning content...
Software systems constantly face situations where the same task can be accomplished in multiple ways. A payment system might support credit cards, PayPal, and cryptocurrency. A compression utility might offer ZIP, GZIP, or LZ4 algorithms. A routing engine might calculate fastest, shortest, or most scenic paths.
The naive approach—embedding all these algorithms in one class with conditional logic—leads to bloated, unmaintainable code. Every new algorithm requires modifying the existing class, violating the Open/Closed Principle and creating a testing nightmare.
The Strategy pattern leverages polymorphism to solve this elegantly: define a family of algorithms, encapsulate each in its own class, and make them interchangeable. The client code selects which algorithm to use without knowing the implementation details.
By the end of this page, you will understand how polymorphism powers the Strategy pattern, how to design strategy hierarchies, when to apply this pattern, and how it appears in real-world systems.
Consider an e-commerce system that calculates shipping costs. Different carriers have different rate calculations, and new carriers are added periodically:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// The naive approach: conditional algorithm selectionclass ShippingCalculator { public double calculateShipping(Order order, String carrier) { double weight = order.getTotalWeight(); double distance = order.getShippingDistance(); // Algorithm selection via conditionals if (carrier.equals("STANDARD")) { // Standard carrier: base rate + weight factor return 5.00 + (weight * 0.50) + (distance * 0.01); } else if (carrier.equals("EXPRESS")) { // Express carrier: premium pricing return 15.00 + (weight * 1.00) + (distance * 0.05); } else if (carrier.equals("ECONOMY")) { // Economy carrier: slow but cheap return 2.00 + (weight * 0.25); } else if (carrier.equals("INTERNATIONAL")) { // International: complex zone-based calculation int zone = getShippingZone(order.getDestination()); double zoneRate = getZoneRate(zone); return 20.00 + (weight * zoneRate) + (distance * 0.10); } else if (carrier.equals("FREIGHT")) { // Freight: volume-based for large items double volume = order.getTotalVolume(); return Math.max(weight, volume) * 2.50 + 50.00; } else { throw new IllegalArgumentException("Unknown carrier: " + carrier); } } // More helper methods... private int getShippingZone(Address address) { /* ... */ } private double getZoneRate(int zone) { /* ... */ }} // Problems:// 1. ShippingCalculator knows ALL carrier algorithms// 2. Adding new carriers requires modifying this class// 3. Each algorithm's logic is interleaved with selection logic// 4. Testing individual algorithms requires testing the whole class// 5. Algorithm reuse in other contexts is impossibleThis approach suffers from what might be called algorithm sprawl—the class grows endlessly as new algorithms are added, and the conditional selection logic becomes increasingly complex and error-prone.
The Strategy pattern refactors conditional algorithms into a polymorphic hierarchy. Each algorithm becomes a class implementing a common interface, and the context object uses this interface polymorphically.
Definition: The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
The pattern has three key participants:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
// Step 1: Define the Strategy interfaceinterface ShippingStrategy { double calculateCost(Order order); String getCarrierName(); int getEstimatedDays(Order order);} // Step 2: Implement Concrete Strategiesclass StandardShippingStrategy implements ShippingStrategy { @Override public double calculateCost(Order order) { double weight = order.getTotalWeight(); double distance = order.getShippingDistance(); return 5.00 + (weight * 0.50) + (distance * 0.01); } @Override public String getCarrierName() { return "Standard Shipping"; } @Override public int getEstimatedDays(Order order) { return 5 + (int)(order.getShippingDistance() / 500); }} class ExpressShippingStrategy implements ShippingStrategy { @Override public double calculateCost(Order order) { double weight = order.getTotalWeight(); double distance = order.getShippingDistance(); return 15.00 + (weight * 1.00) + (distance * 0.05); } @Override public String getCarrierName() { return "Express Shipping"; } @Override public int getEstimatedDays(Order order) { return 1 + (int)(order.getShippingDistance() / 1000); }} class InternationalShippingStrategy implements ShippingStrategy { private final ZoneCalculator zoneCalculator; public InternationalShippingStrategy(ZoneCalculator zoneCalculator) { this.zoneCalculator = zoneCalculator; } @Override public double calculateCost(Order order) { int zone = zoneCalculator.getZone(order.getDestination()); double zoneRate = zoneCalculator.getRate(zone); return 20.00 + (order.getTotalWeight() * zoneRate) + (order.getShippingDistance() * 0.10); } @Override public String getCarrierName() { return "International Shipping"; } @Override public int getEstimatedDays(Order order) { return 7 + zoneCalculator.getZone(order.getDestination()) * 2; }} // Step 3: Context uses Strategy polymorphicallyclass ShippingCalculator { private ShippingStrategy strategy; public ShippingCalculator(ShippingStrategy strategy) { this.strategy = strategy; } // Strategy can be changed at runtime public void setStrategy(ShippingStrategy strategy) { this.strategy = strategy; } public ShippingQuote calculateQuote(Order order) { // Polymorphic call—actual algorithm determined by strategy double cost = strategy.calculateCost(order); String carrier = strategy.getCarrierName(); int days = strategy.getEstimatedDays(order); return new ShippingQuote(carrier, cost, days); }}The Strategy pattern works because of polymorphism. When strategy.calculateCost(order) is called, the JVM dispatches to the actual implementation based on the runtime type of strategy. The caller doesn't know or care which algorithm runs.
Let's see how the Strategy pattern enables flexible algorithm selection and runtime switching:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
// Client code can select and switch strategiespublic class ShippingService { private final Map<String, ShippingStrategy> strategies; public ShippingService() { // Register available strategies strategies = new HashMap<>(); strategies.put("standard", new StandardShippingStrategy()); strategies.put("express", new ExpressShippingStrategy()); strategies.put("international", new InternationalShippingStrategy(new ZoneCalculator())); } // Get all available shipping options for an order public List<ShippingQuote> getAllQuotes(Order order) { List<ShippingQuote> quotes = new ArrayList<>(); for (ShippingStrategy strategy : strategies.values()) { // Each strategy calculates its own quote double cost = strategy.calculateCost(order); String carrier = strategy.getCarrierName(); int days = strategy.getEstimatedDays(order); quotes.add(new ShippingQuote(carrier, cost, days)); } return quotes; } // User selects their preferred shipping method public ShippingQuote getQuote(Order order, String method) { ShippingStrategy strategy = strategies.get(method); if (strategy == null) { throw new IllegalArgumentException("Unknown shipping method"); } return new ShippingQuote( strategy.getCarrierName(), strategy.calculateCost(order), strategy.getEstimatedDays(order) ); } // Add new shipping method without modifying existing code public void registerStrategy(String name, ShippingStrategy strategy) { strategies.put(name, strategy); }} // Usage exampleShippingService service = new ShippingService();Order order = new Order(/* ... */); // Get all shipping optionsList<ShippingQuote> allQuotes = service.getAllQuotes(order);for (ShippingQuote quote : allQuotes) { System.out.println(quote.getCarrier() + ": $" + quote.getCost() + " (" + quote.getDays() + " days)");} // Add a new shipping method at runtimeservice.registerStrategy("drone", new DroneShippingStrategy());The Strategy pattern becomes even more powerful when combined with Dependency Injection (DI). Instead of selecting strategies programmatically, the appropriate strategy is injected based on configuration:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// Spring Framework example@Configurationpublic class ShippingConfig { @Bean @Profile("production") public ShippingStrategy productionShippingStrategy() { // Real shipping calculation in production return new LiveShippingStrategy( new CarrierApiClient(), new RateCalculator() ); } @Bean @Profile("development") public ShippingStrategy developmentShippingStrategy() { // Fixed rates for development testing return new FixedRateShippingStrategy(5.00); } @Bean @Profile("test") public ShippingStrategy testShippingStrategy() { // Predictable strategy for automated tests return new MockShippingStrategy(); }} // Service receives strategy via constructor injection@Servicepublic class OrderService { private final ShippingStrategy shippingStrategy; // DI container injects the appropriate strategy @Autowired public OrderService(ShippingStrategy shippingStrategy) { this.shippingStrategy = shippingStrategy; } public OrderSummary checkout(Cart cart) { Order order = createOrderFromCart(cart); // Same code, different behavior based on injected strategy double shippingCost = shippingStrategy.calculateCost(order); return new OrderSummary(order, shippingCost); }} // The OrderService code is identical across all environments// Only the injected strategy differsWith DI and Strategy combined, you can change system behavior by changing configuration alone—no code modifications required. Different environments, customers, or deployment targets can have completely different algorithm implementations.
Strategy objects can encapsulate arbitrarily complex logic with their own dependencies, state, and helper methods. This is particularly valuable when algorithms require significant supporting infrastructure:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
// Strategy interface for payment processinginterface PaymentStrategy { PaymentResult processPayment(PaymentRequest request); boolean supports(String paymentMethod); void refund(String transactionId, Money amount);} // Complex strategy with its own dependenciesclass CreditCardPaymentStrategy implements PaymentStrategy { private final PaymentGateway gateway; private final FraudDetectionService fraudService; private final CardValidator cardValidator; private final RetryPolicy retryPolicy; private final Logger logger; public CreditCardPaymentStrategy( PaymentGateway gateway, FraudDetectionService fraudService, CardValidator cardValidator, RetryPolicy retryPolicy) { this.gateway = gateway; this.fraudService = fraudService; this.cardValidator = cardValidator; this.retryPolicy = retryPolicy; this.logger = LoggerFactory.getLogger(getClass()); } @Override public PaymentResult processPayment(PaymentRequest request) { // Step 1: Validate card ValidationResult validation = cardValidator.validate(request.getCard()); if (!validation.isValid()) { return PaymentResult.declined(validation.getReason()); } // Step 2: Check for fraud FraudScore fraudScore = fraudService.analyze(request); if (fraudScore.isHighRisk()) { logger.warn("High fraud risk detected: {}", request.getOrderId()); return PaymentResult.declined("Additional verification required"); } // Step 3: Process with retry logic return retryPolicy.execute(() -> { GatewayResponse response = gateway.charge( request.getCard(), request.getAmount() ); if (response.isSuccessful()) { return PaymentResult.approved(response.getTransactionId()); } else { return PaymentResult.declined(response.getErrorMessage()); } }); } @Override public boolean supports(String paymentMethod) { return "credit_card".equals(paymentMethod) || "debit_card".equals(paymentMethod); } @Override public void refund(String transactionId, Money amount) { gateway.refund(transactionId, amount); }} // Another complex strategy with different dependenciesclass CryptoPaymentStrategy implements PaymentStrategy { private final BlockchainClient blockchain; private final WalletService walletService; private final ExchangeRateService exchangeService; private final ConfirmationWaiter confirmationWaiter; // Constructor injection of dependencies... @Override public PaymentResult processPayment(PaymentRequest request) { // Convert to crypto at current rate CryptoAmount cryptoAmount = exchangeService.convert( request.getAmount(), request.getCryptoCurrency() ); // Generate payment address String paymentAddress = walletService.generatePaymentAddress( request.getOrderId() ); // Wait for blockchain confirmation (different flow entirely) boolean confirmed = confirmationWaiter.waitForConfirmation( paymentAddress, cryptoAmount, Duration.ofMinutes(15) ); if (confirmed) { return PaymentResult.approved(paymentAddress); } else { return PaymentResult.timeout("Payment not received in time"); } } // ... other methods}Notice how each strategy encapsulates its own dependencies and logic. The credit card strategy needs fraud detection and card validation; the crypto strategy needs blockchain integration and exchange rate services. Yet both present the same interface to client code—that's polymorphism enabling heterogeneous implementations behind a uniform contract.
The Strategy pattern can be enhanced with two powerful variations: Composite Strategies that combine multiple strategies, and Null Strategies that provide safe no-op behavior.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
// Composite Strategy: combines multiple strategiesclass CompositeDiscountStrategy implements DiscountStrategy { private final List<DiscountStrategy> strategies; public CompositeDiscountStrategy(DiscountStrategy... strategies) { this.strategies = Arrays.asList(strategies); } @Override public Money calculateDiscount(Order order) { // Apply all strategies and sum discounts return strategies.stream() .map(s -> s.calculateDiscount(order)) .reduce(Money.zero(), Money::add); }} // Usage: stack multiple discountsDiscountStrategy memberDiscount = new MemberDiscountStrategy();DiscountStrategy volumeDiscount = new VolumeDiscountStrategy();DiscountStrategy seasonalDiscount = new SeasonalDiscountStrategy(); DiscountStrategy combinedDiscount = new CompositeDiscountStrategy( memberDiscount, volumeDiscount, seasonalDiscount); Money totalDiscount = combinedDiscount.calculateDiscount(order); // ============================================ // Null Object Strategy: safe no-op behaviorclass NoDiscountStrategy implements DiscountStrategy { @Override public Money calculateDiscount(Order order) { return Money.zero(); // No discount applied }} class NoLoggingStrategy implements LoggingStrategy { @Override public void log(LogLevel level, String message) { // Do nothing—intentionally empty } @Override public void log(LogLevel level, String message, Throwable exception) { // Do nothing—intentionally empty }} // With Null Object, no null checks neededclass OrderProcessor { private final DiscountStrategy discountStrategy; private final LoggingStrategy loggingStrategy; public OrderProcessor( DiscountStrategy discountStrategy, LoggingStrategy loggingStrategy) { // Never null—use Null Objects instead this.discountStrategy = discountStrategy != null ? discountStrategy : new NoDiscountStrategy(); this.loggingStrategy = loggingStrategy != null ? loggingStrategy : new NoLoggingStrategy(); } public void process(Order order) { // No null checks needed—both strategies always work loggingStrategy.log(INFO, "Processing order: " + order.getId()); Money discount = discountStrategy.calculateDiscount(order); order.applyDiscount(discount); loggingStrategy.log(INFO, "Applied discount: " + discount); }}The Null Object pattern eliminates null checks by providing a strategy that does nothing. This simplifies client code and prevents NullPointerExceptions. It's a common companion to the Strategy pattern.
The Strategy pattern isn't always the right choice. Understanding when to use it versus simpler conditional logic is essential for pragmatic design:
| Factor | Favor Strategy Pattern | Favor Conditionals |
|---|---|---|
| Number of algorithms | 3+ algorithms, or growing | 1-2 simple algorithms |
| Algorithm complexity | Complex, multi-step algorithms | Simple one-liner calculations |
| Reuse needed | Algorithms used in multiple places | Algorithm used in one place only |
| Testing requirements | Individual algorithm testing needed | Simple enough to test inline |
| Change frequency | Algorithms change/grow frequently | Stable, rarely-changing logic |
| Runtime switching | Need to swap algorithms dynamically | Algorithm fixed at design time |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// DON'T use Strategy for trivial conditionals// This is OVER-ENGINEERING: interface PositiveNegativeZeroStrategy { String classify(int number);} class PositiveStrategy implements PositiveNegativeZeroStrategy { public String classify(int number) { return "positive"; }} class NegativeStrategy implements PositiveNegativeZeroStrategy { public String classify(int number) { return "negative"; }} class ZeroStrategy implements PositiveNegativeZeroStrategy { public String classify(int number) { return "zero"; }} // JUST USE A SIMPLE CONDITIONAL:String classify(int number) { if (number > 0) return "positive"; if (number < 0) return "negative"; return "zero";} // ============================================ // DO use Strategy when complexity warrants it: // Multiple complex validation rules that vary by contextinterface ValidationStrategy { ValidationResult validate(UserInput input);} class StrictValidationStrategy implements ValidationStrategy { // 50 lines of strict validation logic} class LenientValidationStrategy implements ValidationStrategy { // 50 lines of lenient validation logic} class AdminValidationStrategy implements ValidationStrategy { // 50 lines of admin-specific validation logic}Creating strategy classes for trivial logic is over-engineering. The pattern adds indirection and complexity—only use it when that complexity pays for itself through improved maintainability, testability, or flexibility.
The Strategy pattern is pervasive in professional software and frameworks:
| System/Framework | Strategy Interface | Concrete Strategies |
|---|---|---|
| Java Collections | Comparator<T> | Natural order, custom comparators |
| Java Concurrency | RejectedExecutionHandler | Abort, CallerRuns, Discard policies |
| Spring Security | AuthenticationProvider | DAO, LDAP, OAuth providers |
| Hibernate | Dialect | MySQL, PostgreSQL, Oracle dialects |
| AWS SDK | RetryPolicy | Standard, adaptive, legacy retry |
| React | Reconciliation algorithm | Stack, Fiber reconcilers |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// Java's Comparator is a Strategy for sortingList<Person> people = getPeople(); // Different sorting strategiesComparator<Person> byAge = Comparator.comparing(Person::getAge);Comparator<Person> byName = Comparator.comparing(Person::getName);Comparator<Person> byAgeDescending = byAge.reversed(); // Same operation, different strategyCollections.sort(people, byAge); // Sort by ageCollections.sort(people, byName); // Sort by nameCollections.sort(people, byAgeDescending); // Sort by age descending // ============================================ // ThreadPoolExecutor's rejection handling is a StrategyThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), // Strategy: what to do when queue is full new ThreadPoolExecutor.CallerRunsPolicy() // Run in caller's thread // Alternative strategies: // new ThreadPoolExecutor.AbortPolicy() // Throw exception // new ThreadPoolExecutor.DiscardPolicy() // Silently discard // new ThreadPoolExecutor.DiscardOldestPolicy() // Discard oldest in queue); // ============================================ // Spring's AuthenticationProvider is a Strategy@Configurationpublic class SecurityConfig { @Bean public AuthenticationProvider databaseAuthProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(passwordEncoder); return provider; } @Bean public AuthenticationProvider ldapAuthProvider() { return new LdapAuthenticationProvider(ldapAuthenticator); } // Multiple authentication strategies can be configured}You've likely been using Strategy pattern without knowing it. Every time you pass a Comparator to a sort method or configure a validation policy, you're applying the Strategy pattern.
The Strategy pattern demonstrates polymorphism's power to make behavior interchangeable. Where Factory Method makes creation polymorphic, Strategy makes algorithms polymorphic—and both rely on the same underlying mechanism of dynamic dispatch.
What's next:
In the next page, we'll explore polymorphic collections—how collections of objects typed to an interface can contain heterogeneous implementations, enabling powerful patterns for processing diverse object types uniformly.
You now understand how polymorphism powers the Strategy pattern, enabling interchangeable algorithms that can be selected, swapped, and configured at runtime. This pattern is foundational to flexible software design.