RizTech Academy logo
RizTech Academy
Design Patterns in PracticeLesson 4 of 735 min

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

  1. Rewrite the if/else billing above as a Map<Plan, Pricing>. Add a fourth plan and count the files you touched in each version.
  2. Add a second abstract method to Pricing and watch what @FunctionalInterface does.
  3. Write the student pricing as a named CappedPricing class taking the rate and the cap. Write a test for the cap boundary.
  4. Sort the capstone's deliveries three ways with three Comparator lambdas.
  5. Write one of those comparators as a full class implementing Comparator. Compare the two.
  6. Implement MonthlyReport twice — a customer bill and a per-day summary.
  7. Remove final from render and override it in a subclass. Say what has been lost.
  8. Look at AbstractList in the JDK and list the methods written in terms of get and size.
  9. Take a method of yours with a long if/else on a type or enum. Decide whether strategy would improve it — and be willing to answer no.
  10. 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