LLD Dojo

Patterns you will actually be asked for · chapter 19 of 33

Strategy: behaviour you can swap

Chapter 3.1 · Part 3, Patterns you will actually be asked for · about 30 minutes

What you need before this chapter: Part 1 in full, especially interfaces (1.4). Part 2 in full, especially open/closed (2.5) and dependency inversion (2.8). This chapter is what both of those principles look like once you actually build something with them.

When you finish this chapter you will be able to:


1. The situation

A parking lot charges a fee when a car leaves. Somebody has to write the method that works out how much. Whatever else this system will eventually do, that fee calculation has to exist, so it is a reasonable place to start.

static long charge(Duration stayed) {
    long hours = Math.max(1, (stayed.toMinutes() + 59) / 60);
    return hours * 50;
}

Math.max(1, ...) rounds a partial hour up and guarantees even a two-minute stay counts as one full hour, which is a real business rule and not an accident of the arithmetic. Fifty is the rate, in whatever currency unit the lot uses, per hour.

2. Naive code that is fine

Compile it and run it against two stays.

javac Step1.java
java Step1
2 hours: 100
90 minutes: 100

Both come back as 100, and that is correct: two full hours costs 100, and 90 minutes rounds up to two started hours, which also costs 100. One method, one rule, nothing to argue with. If this were the whole system, there would be nothing left to improve.

3. A new requirement

The lot starts admitting motorbikes, and motorbikes cost less: 20 an hour instead of 50. The charge method now has to know which kind of vehicle it is pricing, so it takes a second argument and branches on it.

enum VehicleType { MOTORBIKE, CAR }

static long charge(Duration stayed, VehicleType type) {
    long hours = Math.max(1, (stayed.toMinutes() + 59) / 60);
    if (type == VehicleType.MOTORBIKE) return hours * 20;
    return hours * 50;
}
car, 2 hours: 100
motorbike, 2 hours: 40

This is still completely reasonable code. One if, two cases, and a reader checks it against the requirement in about two seconds. Nobody should feel bad about writing this for a two-case rule.

4. Watch where it goes, and the real cost

Trucks join next, at 90 an hour. That is a second if, or a switch with a third arm — still manageable. Then the business wants an overnight flat rate, and after that a members' discount. Each one adds a branch to the same method, and charge slowly turns into the one place that holds every pricing rule the lot has ever been asked to support.

That growth is annoying, but it is survivable on its own. The change that actually forces a redesign is a second piece of code needing the same rule. Say a receipt kiosk near the exit wants to show a driver the estimated fee before they pay, so they are not surprised at the barrier. The kiosk is not the class that owns a ticket or collects payment; it just wants an answer to "what would this cost." It cannot call into the middle of charge, because charge is one method on one class, built to run once a stay is over. The only way to get the estimate is to copy the branches: motorbike gets 20 an hour, everything else gets 50, truck gets 90 once that arm exists.

Now the pricing rule lives in two places that have no connection to each other. Take the fifth requirement: a members' discount. It lands as an edit to charge. Whoever makes it either remembers there is a second copy in the kiosk and updates both, or does not, and the kiosk quietly starts quoting the wrong price. Nothing about the language or the compiler catches that mistake. Two methods that are supposed to agree, agreeing only by discipline, is the cost that matters here, more than the length of charge on its own.

5. The move

Look at what is actually changing from one requirement to the next. It is never the sequence "take a duration, produce a fee" — that shape has been stable since section 1. What changes, every time, is the rule for turning a stay into a number. So give that one varying piece of behaviour a name of its own, as an interface, and make everything that needs a fee depend on the interface rather than on a particular rule.

interface PricingPolicy {
    long feeMinor(VehicleType type, Duration stayed);
}

An interface with one method states a contract: anything that claims to be a PricingPolicy can answer this one question, however it works out the answer internally. Today's rule becomes the first implementation, and nothing about it looks different from an ordinary class.

final class FlatHourlyPricing implements PricingPolicy {
    @Override
    public long feeMinor(VehicleType type, Duration stayed) {
        long hours = Math.max(1, (stayed.toMinutes() + 59) / 60);
        return switch (type) {
            case MOTORBIKE -> hours * 20;
            case CAR -> hours * 50;
            case TRUCK -> hours * 90;
        };
    }
}

The classes that need a fee stop containing the rule and start holding a PricingPolicy instead, handed to them once, in the constructor.

final class Till {
    private final PricingPolicy pricing;

    Till(PricingPolicy pricing) {
        this.pricing = Objects.requireNonNull(pricing);
    }

    long charge(VehicleType type, Duration stayed) {
        return pricing.feeMinor(type, stayed);
    }
}

final class ReceiptKiosk {
    private final PricingPolicy pricing;

    ReceiptKiosk(PricingPolicy pricing) {
        this.pricing = Objects.requireNonNull(pricing);
    }

    String preview(VehicleType type, Duration stayed) {
        return "Estimated fee: " + pricing.feeMinor(type, stayed) + " rupees";
    }
}

Both classes are handed the exact same PricingPolicy object at start-up.

javac Step3.java
java Step3
till charge, car 2h: 100
Estimated fee: 100 rupees
till charge, truck 90m: 180

Till and ReceiptKiosk agree on the price for the same stay, because they are not each holding an opinion about pricing — they are both asking the one object whose job that is. A members' discount is now a second class implementing PricingPolicy, wired in wherever the lot decides to use it. Neither Till nor ReceiptKiosk is edited to make room for it, which is exactly the "open for extension, closed for modification" idea from chapter 2.5, now with a shape you can build.

This is the Strategy pattern. There is nothing more to it than what you have just built: an interface for the one thing that varies, one implementation per rule, and a holder that depends on the interface without knowing which rule it has been given.

6. What modern Java changes here

PricingPolicy has exactly one abstract method, which makes it a functional interface, the same idea chapter 1.4 introduced. Anything with one abstract method can be satisfied by a lambda instead of a named class, with no implements clause and no @Override anywhere.

Till freeToday = new Till((type, stayed) -> 0L);
free promotion, car 2h: 0

This is still Strategy — the seam is the interface, not the syntax used to fill it in. Prefer a named class when the rule has a name worth reading at the call site, or when it needs its own data the way FlatHourlyPricing would if its rates were configurable. Prefer a lambda when the rule really is one expression with nothing to remember between calls. The pattern catalogue this idea comes from was written in 1994, well before Java had lambdas at all, so a strategy learned only from the book tends to come out as a needless class where a one-line lambda would read better.

7. When naming it is wrong

One implementation, with no second one anywhere in sight, is not Strategy. It is a field with extra steps: an interface that exists to be satisfied by exactly one class does not decouple anything, because nothing is varying. This app's own grading standard penalises that as hard as it penalises having no seam at all — a speculative interface with one implementation scores lower than the plain method it replaced, tagged over-engineered (premature interface).

One question settles it before you write the interface: can you name the second implementation right now, and write the one method signature both of them would satisfy? If the answer is no, the interface is a guess about the future, and waiting to write it costs you nothing. The flat-rate charge method from section 2 was correct code, not a mistake waiting to be caught — it only became worth replacing once a second rule and a second caller both existed.

Your turn

Add an overnight rate: any vehicle parked overnight pays a flat 100, regardless of type or how many hours it actually stayed. Write it as a new class implementing PricingPolicy, and wire it into a Till without touching Till itself.

Do this before reading on.

The answer.

final class OvernightFlatPricing implements PricingPolicy {
    @Override
    public long feeMinor(VehicleType type, Duration stayed) {
        return 100L;
    }
}
Till overnightTill = new Till(new OvernightFlatPricing());
System.out.println("overnight, car 9h: " + overnightTill.charge(VehicleType.CAR, Duration.ofHours(9)));
System.out.println("overnight, truck 11h: " + overnightTill.charge(VehicleType.TRUCK, Duration.ofHours(11)));
overnight, car 9h: 100
overnight, truck 11h: 100

If your output matches, notice what did not happen: Till's source was not opened. A brand-new pricing rule, ignoring an argument FlatHourlyPricing cared about, slotted in as one new file.

Going deeper

A lambda strategy is a real object, not syntax sugar that disappears at compile time, and it can hold onto values from the method that created it without ever declaring a field. This is worth seeing directly, because it changes what a strategy can do and what a test can assume about it.

static PricingPolicy discounted(long percentOff) {
    long baseRatePerHour = 50;
    return stayed -> {
        long hours = Math.max(1, (stayed.toMinutes() + 59) / 60);
        long full = hours * baseRatePerHour;
        return full - (full * percentOff / 100);
    };
}

percentOff and baseRatePerHour are ordinary local variables, not fields on any class. Java allows a lambda to read a local from its enclosing method only if that local is effectively final, meaning it is never reassigned after it is set. When it does, the lambda keeps its own private copy, called a capture, that survives after discounted has returned. This is exactly what lets you build a family of related strategies from one factory method, each with its own baked-in discount, with no class written anywhere for 10PercentOffPricing or 20PercentOffPricing.

javac Step6.java
java Step6
10% off, 2h: 90
20% off, 2h: 80
same object? false
tenPercentOff.equals(anotherTenPercentOff)? false

The last line is the part worth sitting with. discounted(10) called twice produces two lambdas that compute the exact same answer for every input, and they are still not equal to each other. Neither overrides equals, so both fall back to identity: two different objects, even with identical behaviour, compare unequal. A test that wants to check "this Till was configured with a 10% discount policy" cannot assert equality on the policy object. It has to call feeMinor with a known duration and check the number that comes back. That is a real constraint on how you test code built this way. It is the reason a strategy that needs to be compared, logged meaningfully, or looked up in a Set is a case for a named class over a lambda, whatever section 6 said about brevity.

Why this matters in an interview

Strategy is usually the first pattern an interviewer expects you to reach for, because most systems they ask you to design have at least one rule that plausibly varies: a pricing tier, a shipping policy, a matching algorithm. The candidates who do well are not the ones who name the pattern fastest. They are the ones who can point at the actual second requirement that justified the interface. They can also say, when asked "would you always do this," that a single fixed rule with no second case in view does not need one.


Next: chapter 3.2, Factory: construction kept away from use. Once a class needs several collaborators wired together correctly, and needs to be built more than one way, the question stops being "what varies" and becomes "who is responsible for putting the pieces together."

← 2.8 Dependency inversion, and injecting the clock · All chapters · 3.2 Factory: construction kept away from use →