When to Use Strategy?
4. Auto-registry in Spring via List 5. Context Object for data passing 6. Avoid in hot path (>5 implementations — HotSpot threshold) 7. Template Method for parts of an algorithm...
Junior Level
Strategy is a pattern that allows selecting different algorithms for the same task.
Simple analogy: GPS navigator. The goal is the same (get from A to B), but routes differ: by car, on foot, by public transport.
Example:
// Strategy interface
interface PaymentStrategy {
void pay(BigDecimal amount);
}
// Concrete strategies
class CreditCardStrategy implements PaymentStrategy {
public void pay(BigDecimal amount) {
System.out.println("Paid " + amount + " by credit card");
}
}
class PayPalStrategy implements PaymentStrategy {
public void pay(BigDecimal amount) {
System.out.println("Paid " + amount + " via PayPal");
}
}
// Usage
PaymentStrategy strategy = new CreditCardStrategy();
strategy.pay(new BigDecimal("100"));
// Easy to swap
strategy = new PayPalStrategy();
strategy.pay(new BigDecimal("200"));
When to use:
- Multiple ways to do the same thing
- Need to choose algorithm at runtime
- Lots of
if-elseorswitch
Middle Level
Problem: Growing switch
// Bad: adding a new type = modifying the method
public void processPayment(PaymentType type, BigDecimal amount) {
switch (type) {
case CREDIT_CARD:
processCreditCard(amount);
break;
case PAYPAL:
processPayPal(amount);
break;
case CRYPTO:
processCrypto(amount);
break;
// Each new type -> method modification!
}
}
Solution: Strategy
// Good: adding a new type = new class
interface PaymentStrategy {
void pay(BigDecimal amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(BigDecimal amount) { /* logic */ }
}
class PayPalPayment implements PaymentStrategy {
public void pay(BigDecimal amount) { /* logic */ }
}
// Strategy selection
Map<PaymentType, PaymentStrategy> strategies = Map.of(
CREDIT_CARD, new CreditCardPayment(),
PAYPAL, new PayPalPayment(),
CRYPTO, new CryptoPayment()
);
strategies.get(type).pay(amount);
Modern Java: Lambdas
// For simple strategies, no classes needed!
Map<PaymentType, Consumer<BigDecimal>> strategies = Map.of(
CREDIT_CARD, amount -> processCreditCard(amount),
PAYPAL, amount -> processPayPal(amount),
CRYPTO, amount -> processCrypto(amount)
);
strategies.get(type).accept(amount);
Enum Strategy
// For a limited set of strategies
public enum ShippingStrategy {
AIR {
public BigDecimal calculate(BigDecimal weight) {
return weight.multiply(BigDecimal.TEN);
}
},
SEA {
public BigDecimal calculate(BigDecimal weight) {
return weight.multiply(BigDecimal.ONE);
}
};
public abstract BigDecimal calculate(BigDecimal weight);
}
// Usage
BigDecimal cost = ShippingStrategy.AIR.calculate(weight);
Spring: Auto-registry Strategy
// Automatic registration of all strategies
public interface DiscountStrategy {
BigDecimal apply(BigDecimal amount);
String getType();
}
@Component
public class PercentageDiscount implements DiscountStrategy {
public String getType() { return "PERCENTAGE"; }
public BigDecimal apply(BigDecimal amount) { return amount.multiply(new BigDecimal("0.9")); }
}
@Component
public class FixedDiscount implements DiscountStrategy {
public String getType() { return "FIXED"; }
public BigDecimal apply(BigDecimal amount) { return amount.subtract(new BigDecimal("100")); }
}
// Automatic Map
@Service
public class DiscountService {
private final Map<String, DiscountStrategy> strategies;
public DiscountService(List<DiscountStrategy> allStrategies) {
this.strategies = allStrategies.stream()
.collect(Collectors.toMap(
DiscountStrategy::getType,
Function.identity()
));
}
public BigDecimal applyDiscount(String type, BigDecimal amount) {
return strategies.get(type).apply(amount);
}
}
// Add a new strategy = create a new @Component!
Typical Mistakes
- Strategy for simple if-else
// Overengineering interface CompareStrategy { int compare(int a, int b); } class AscendingCompare implements CompareStrategy { ... } class DescendingCompare implements CompareStrategy { ... } // Lambda is sufficient Comparator<Integer> asc = Integer::compare; Comparator<Integer> desc = (a, b) -> Integer.compare(b, a); - Fat interface
// Too many methods interface Strategy { void init(); void process(); void cleanup(); String getName(); int getPriority(); } // Split into separate strategies
Senior Level
Strategy vs State vs Template Method
Strategy vs State:
// Strategy: chosen externally
order.setPaymentStrategy(new CreditCardPayment()); // Client chooses
// State: changed internally
order.setState(PaidState.INSTANCE); // Object changes state itself
order.process(); // Behavior depends on state
Strategy vs Template Method:
// Template Method: inheritance, parts of algorithm
abstract class AbstractReport {
public void generate() {
loadData(); // Overridden
formatData(); // Overridden
saveReport(); // Common code
}
}
// Strategy: composition, entire algorithm
interface ReportGenerator { Report generate(); }
class PdfReportGenerator implements ReportGenerator { ... }
class HtmlReportGenerator implements ReportGenerator { ... }
JIT and Polymorphism
// Monomorphic call (1 implementation) -> inlining
PaymentStrategy strategy = new CreditCardPayment();
strategy.pay(amount); // JIT inlines code -> 0 overhead
// Bimorphic call (2 implementations) -> inline cache
// JIT still optimizes
// Megamorphic call (>5 implementations — HotSpot threshold) -> vtable lookup
interface PaymentStrategy { void pay(); }
class A implements PaymentStrategy { ... }
class B implements PaymentStrategy { ... }
class C implements PaymentStrategy { ... }
class D implements PaymentStrategy { ... } // >2!
// JIT cannot inline -> indirect call
// -> ~5-10ns overhead per call
// -> In hot path (1M calls/sec) = 5-10ms loss
Hot-path optimization:
// Instead of interface -> enum switch
public enum PaymentType {
CREDIT_CARD, PAYPAL, CRYPTO
}
public void process(PaymentType type, BigDecimal amount) {
switch (type) {
case CREDIT_CARD -> processCreditCard(amount);
case PAYPAL -> processPayPal(amount);
case CRYPTO -> processCrypto(amount);
}
}
// -> JIT compiles to tableswitch -> O(1) -> fast!
Context Object Pattern
// Problem: strategy needs lots of data
interface PricingStrategy {
BigDecimal calculate(Product p, Customer c, Cart cart,
Promotion promo, Locale locale);
}
// Solution: context object
public record PricingContext(
Product product,
Customer customer,
Cart cart,
Promotion promotion,
Locale locale
) {}
interface PricingStrategy {
BigDecimal calculate(PricingContext context);
}
// Adding new data doesn't break the interface!
Production Experience
Real scenario #1: Megamorphic slowdown
- 10 DiscountStrategy implementations
- Hot path: 100,000 calls/sec
- Problem: virtual call overhead ~5-10ns (nanoseconds), not milliseconds
- Solution: enum switch instead of interface
- Result: 10x speedup
Real scenario #2: Spring auto-registry
- 20 validation strategies
- Manual Map registration -> errors
- Solution: auto-registry via List injection
- Result: new strategy = new @Component
Best Practices
- Lambdas for simple strategies
- Enum for a limited set
- Classes for complex logic
- Auto-registry in Spring via List
- Context Object for data passing
- Avoid in hot path (>5 implementations — HotSpot threshold)
- Template Method for parts of an algorithm
- Strategy for entire algorithm replacement
Senior Summary
- Strategy = replacing the entire algorithm via composition
- State = behavior change from within
- Template Method = replacing parts via inheritance
- JIT: megamorphic calls >5 implementations (HotSpot threshold) -> overhead
- Hot path: enum switch > interface
- Context Object for passing data to strategy
- Spring auto-registry: List
-> Map - Modern Java: lambdas replace simple strategies
Interview Cheat Sheet
Must know:
- Strategy replaces growing switch/if-else via composition — each algorithm in a separate class
- In Modern Java, lambdas replace simple strategies: Map<Enum, Function> instead of 10+ classes
- Spring auto-registry: List
injection -> automatic Map<String, Strategy> - JIT optimization: megamorphic calls (>5 implementations — HotSpot threshold) -> virtual call overhead ~5-10ns
- For hot paths: enum switch is faster than interface (JIT compiles to tableswitch -> O(1))
- Context Object Pattern: passing data to strategy via context object instead of 5+ parameters
- Enum Strategy — for a limited set of algorithms with compact code
Common follow-up questions:
- When is Strategy overengineering? — Simple if-else with 2-3 branches and uncomplicated logic
- How does Strategy differ from State? — Strategy is chosen externally (client), State changes internally (object itself)
- What is a megamorphic call? — Method call with >5 implementations (HotSpot threshold) -> JIT doesn’t inline -> overhead
- How does Spring auto-register strategies? — List
injection -> Stream.collect(toMap)
Red flags (DO NOT say):
- “I use Strategy for 2 if-else branches” — overengineering, lambdas are enough
- “Strategy doesn’t affect performance” — megamorphic calls >5 implementations have measurable overhead
- “I don’t use Strategy, I write switch” — OCP violation, adding a new type = code modification
- “All strategies must be in separate classes” — lambdas and enum replace simple strategies
Related topics:
- [[13. Difference between State and Strategy]] — pattern comparison
- [[01. What are design patterns]] — lambdas replace Strategy
- [[02. What pattern categories exist]] — Behavioral patterns
- [[16. What anti-patterns do you know]] — Golden Hammer
- [[14. What Proxy types exist]] — alternative Structural approach