Strategy and template method, with lambdas
Two patterns for the same problem — part of an algorithm needs to vary — that solve it in opposite directions. One composes, one inherits, and modern Java has strong opinions about which you should reach for.
Strategy
Pull the varying part out into its own type and pass it in.
The tiffin service charges differently for different plans. The naive version:
long billPaise(Plan plan, int tiffins, int daysInMonth) {
if (plan == Plan.VEG) {
return 8_000L * tiffins;
} else if (plan == Plan.JAIN) {
return 9_000L * tiffins;
} else if (plan == Plan.STUDENT) {
// 10% off, and never more than a monthly cap
long raw = (long) (7_400L * tiffins * 0.9);
return Math.min(raw, 180_000L);
}
throw new IllegalArgumentException("unknown plan " + plan);
}
Every new plan edits this method. The pricing rules for three unrelated plans sit on top of each other. And the method has to end with a throw for a case the compiler could have ruled out.
Strategy makes each rule its own thing:
@FunctionalInterface
public interface Pricing {
long billPaise(int tiffins);
}
public final class BillingService {
private final Map<Plan, Pricing> pricing;
public BillingService(Map<Plan, Pricing> pricing) {
this.pricing = Map.copyOf(pricing);
}
public long billPaise(Plan plan, int tiffins) {
Pricing rule = pricing.get(plan);
if (rule == null) {
throw new IllegalArgumentException("no pricing for " + plan);
}
return rule.billPaise(tiffins);
}
}
And the rules are values:
Map<Plan, Pricing> rules = Map.of(
Plan.VEG, tiffins -> 8_000L * tiffins,
Plan.JAIN, tiffins -> 9_000L * tiffins,
Plan.STUDENT, tiffins -> Math.min((long) (7_400L * tiffins * 0.9), 180_000L));
@FunctionalInterface is the important annotation. It does not make the
interface usable as a lambda — one abstract method does that — but it makes the
compiler refuse if somebody later adds a second abstract method, which would
break every lambda at once.
You have been using this since module 6
deliveries.sort(Comparator.comparing(Delivery::date));
Comparator is a strategy. sort does not know how to compare deliveries; it
knows how to sort given something that does. That separation is the entire
pattern, and the JDK is built on it — Comparator, Predicate, Function,
Runnable, every argument to Stream.map.
Before Java 8, each of those meant a named class or an anonymous inner class. Strategy used to be a diagram. It is now an argument.
When a class still beats a lambda
Lambdas are right when the strategy is a short expression and needs no state. A named class is better when:
- the rule is long enough to need its own tests,
- it holds configuration (
new CappedPricing(7_400L, 180_000L)), - the same rule is used in several places, or
- the lambda would need a comment to explain it.
A lambda spanning fifteen lines is a method that has not been given a name.
Template method
The opposite arrangement: the skeleton is fixed and the steps vary.
public abstract class MonthlyReport {
// final on purpose. This is the algorithm, and subclasses supply steps
// rather than rearranging them.
public final String render(List<Delivery> deliveries, YearMonth month) {
StringBuilder sb = new StringBuilder();
sb.append(title(month)).append('\n');
sb.append("=".repeat(title(month).length())).append('\n');
for (String line : body(deliveries, month)) {
sb.append(line).append('\n');
}
sb.append(footer(deliveries, month)).append('\n');
return sb.toString();
}
protected abstract String title(YearMonth month);
protected abstract List<String> body(List<Delivery> deliveries, YearMonth month);
// A default a subclass may override, which is the other half of the shape.
protected String footer(List<Delivery> deliveries, YearMonth month) {
return "Generated " + LocalDate.now();
}
}
The final on render is what makes it a template method rather than just an
abstract class. It says: this order is the design, and you get to fill in steps,
not change the sequence.
Java's own AbstractList works this way — implement get and size, and
iterator, indexOf, contains and the rest come free, all written in terms
of those two.
Which one, and why usually strategy
| Strategy | Template method | |
|---|---|---|
| Mechanism | composition | inheritance |
| Varying part | a whole algorithm | steps inside a fixed one |
| Swappable at runtime | yes | no, it is the class |
| Number of varying parts | one per strategy | several at once |
| Lambda-friendly | yes | no |
Prefer strategy. Composition beats inheritance here for the ordinary
reasons: you get one extends, and spending it means a subclass can never
extend anything else. Strategies can be combined, swapped at runtime, and tested
alone. Template method locks the relationship in at compile time.
Template method earns its place when there are genuinely several steps varying together and a fixed order they must run in — a report format, a request lifecycle, a test harness. Passing five lambdas to a method is worse than one subclass supplying five.
The honest summary: if you can express it with one lambda, do. If you are about to pass three, consider template method. If you are passing five, definitely.
Check your work
What strategy separates: the algorithm from the code that uses it.
Why @FunctionalInterface matters: it makes the compiler refuse a second
abstract method, which would break every lambda at once.
Where you have already used strategy: every Comparator, Predicate and
Function in the JDK.
What Java 8 changed: strategy went from a class hierarchy to an argument.
When a named class beats a lambda: it needs tests, holds configuration, is reused, or would need a comment.
What makes a template method a template method: the skeleton method is
final, so subclasses fill in steps rather than reordering them.
Why strategy is usually the better default: composition — you only get one
extends, and strategies swap at runtime and test alone.
When template method earns its place: several steps varying together in a fixed order.
Practice
- Rewrite the
if/elsebilling above as aMap<Plan, Pricing>. Add a fourth plan and count the files you touched in each version. - Add a second abstract method to
Pricingand watch what@FunctionalInterfacedoes. - Write the student pricing as a named
CappedPricingclass taking the rate and the cap. Write a test for the cap boundary. - Sort the capstone's deliveries three ways with three
Comparatorlambdas. - Write one of those comparators as a full class implementing
Comparator. Compare the two. - Implement
MonthlyReporttwice — a customer bill and a per-day summary. - Remove
finalfromrenderand override it in a subclass. Say what has been lost. - Look at
AbstractListin the JDK and list the methods written in terms ofgetandsize. - Take a method of yours with a long
if/elseon a type or enum. Decide whether strategy would improve it — and be willing to answer no. - Write a method taking four lambdas. Then rewrite it as a template method and argue which you would rather maintain.
Next: three patterns for making things fit together.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship