The Strategy Pattern is a classic design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. Traditionally implemented using separate classes, Java 8 introduced lambda expressions, making the Strategy Pattern more elegant and concise.
In this tutorial, we’ll explore how to implement the Strategy Pattern using lambdas and functional interfaces, boosting readability and reducing boilerplate.
🎯 What Is the Strategy Pattern?
The Strategy Pattern enables behavioral variation without modifying the client code. It's based on:
- A common interface for all strategies
- Multiple concrete strategy implementations
- Runtime selection of the appropriate strategy
💡 Why Use Lambdas for Strategy?
Before Java 8, implementing Strategy required multiple classes. With lambdas, you can pass behavior directly:
- No need to create concrete classes
- Reduces lines of code
- Behavior is now first-class — passed like data
🧱 Traditional Strategy Pattern (Pre-Java 8)
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card");
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
class ShoppingCart {
void checkout(PaymentStrategy strategy, int amount) {
strategy.pay(amount);
}
}
✅ Strategy Pattern with Lambdas (Java 8+)
@FunctionalInterface
interface PaymentStrategy {
void pay(int amount);
}
class ShoppingCart {
void checkout(PaymentStrategy strategy, int amount) {
strategy.pay(amount);
}
}
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.checkout(amount -> System.out.println("Paid " + amount + " with Credit Card"), 500);
cart.checkout(amount -> System.out.println("Paid " + amount + " with PayPal"), 300);
}
}
Cleaner, right?
🔧 Using Built-In Functional Interfaces
Java’s java.util.function
package offers ready-to-use functional interfaces:
Consumer<T>
→ performs action on a valueFunction<T, R>
→ transforms input to outputPredicate<T>
→ returns booleanBiFunction<T, U, R>
→ works with two inputs
BiFunction<Integer, Integer, Integer> addition = (a, b) -> a + b;
System.out.println("Sum = " + addition.apply(3, 5));
🧪 Real-World Example: Discount Strategies
@FunctionalInterface
interface DiscountStrategy {
double apply(double price);
}
public class Checkout {
public double getFinalPrice(double price, DiscountStrategy strategy) {
return strategy.apply(price);
}
}
Usage
Checkout checkout = new Checkout();
double finalPrice1 = checkout.getFinalPrice(1000, p -> p * 0.9); // 10% discount
double finalPrice2 = checkout.getFinalPrice(1000, p -> p - 200); // Flat 200 off
System.out.println(finalPrice1); // 900.0
System.out.println(finalPrice2); // 800.0
🔁 Switching Strategy at Runtime
Map<String, DiscountStrategy> strategies = new HashMap<>();
strategies.put("percentage", p -> p * 0.9);
strategies.put("flat", p -> p - 100);
String userChoice = "flat";
double price = 500;
double discounted = strategies.get(userChoice).apply(price);
System.out.println("Final: " + discounted);
🔗 Strategy with Streams
List<String> items = List.of("apple", "banana", "cherry");
Function<String, String> formatter = s -> s.toUpperCase();
List<String> result = items.stream()
.map(formatter)
.collect(Collectors.toList());
🧠 Benefits of Lambdas in Strategy
- Concise: Reduces boilerplate
- Flexible: Behavior can be passed dynamically
- Composable: Chain strategies via
andThen()
,compose()
- Cleaner APIs: Business logic separated from infrastructure
📌 What's New in Java?
Java 8
- Lambdas, Streams,
java.util.function
, method references
Java 9
Optional.ifPresentOrElse
Java 11
var
in lambda parameters
Java 17
- Pattern matching enhancements, sealed interfaces for strategy type safety
Java 21
- Structured concurrency and scoped values fit well with functional pipelines
✅ Conclusion and Key Takeaways
- The Strategy Pattern enables algorithm flexibility at runtime.
- Lambdas simplify strategy implementations without separate class definitions.
- Built-in functional interfaces make strategies even more reusable.
- Ideal for discounts, payments, event handling, and formatting logic.
❓ Expert FAQ
Q1: Can I use method references as strategies?
Yes, if the method signature matches the functional interface.
Q2: Are lambdas slower than traditional strategies?
No—lambdas are often faster due to invokedynamic
and JVM optimizations.
Q3: What’s the best interface for binary operations?BiFunction<T, U, R>
or BinaryOperator<T>
if input and output types match.
Q4: Is there a performance gain using lambdas in strategy?
They reduce class loading and enable better JIT inlining.
Q5: Can I use lambdas with Spring beans?
Yes, lambdas can be injected or passed as beans with method references.
Q6: Do lambdas support checked exceptions?
No. You must handle or wrap them.
Q7: Can lambdas be serialized in strategies?
Technically yes, but not recommended. Use serializable classes if needed.
Q8: What is a functional interface?
An interface with one abstract method. Needed for lambdas.
Q9: Is the Strategy Pattern obsolete in functional programming?
Not at all—it’s just more elegant now.
Q10: What are anti-patterns in lambda-based strategy?
Over-chaining, capturing heavy outer objects, or burying logic inline.