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 eventually does, the fee calculation has to exist. So start there.

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

Two things in that method are worth noticing. Math.max(1, ...) rounds a partial hour up, so a two-minute stay counts as one full hour. That is a real business rule, not an accident of the arithmetic. And 50 is the rate per hour, in whatever unit the lot charges in.

2. Naive code that is fine

Compile it. Run it against two stays.

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

Both come back as 100, and both are right. Two full hours costs 100. Ninety 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 you would be finished.

3. A new requirement

The lot starts admitting motorbikes, and motorbikes cost less. Twenty an hour instead of fifty.

So charge needs to know what it is pricing. It takes a second argument and branches.

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.

Do not feel bad about writing this for a two-case rule. You should write this.

4. Watch where it goes, and the real cost

Trucks join next, at 90 an hour. That is a second if, or a third arm on a switch. Still fine.

Then the business wants an overnight flat rate. Then a members' discount. Each one adds a branch to the same method, and charge slowly becomes the one place holding every pricing rule the lot has ever been asked for.

Annoying, and survivable on its own.

Here is the change that actually forces a redesign. A second piece of code needs the same rule.

A receipt kiosk near the exit wants to show a driver the estimated fee before they pay, so nobody is surprised at the barrier. The kiosk does not own a ticket and does not collect payment. It only wants an answer to "what would this cost".

It cannot call into the middle of charge. charge is one method on one class, built to run after a stay has ended.

So whoever writes the kiosk copies the branches. Motorbike 20, car 50, truck 90.

Now stop and see where you are. The pricing rule lives in two places with no connection between them.

The fifth requirement arrives: a members' discount. It lands as an edit to charge. Whoever makes that edit either remembers the second copy in the kiosk, or does not, and the kiosk quietly starts quoting the wrong price.

Nothing in the language catches that. Two methods that must agree, agreeing only by discipline — that is the cost that matters here, far more than the length of charge.

5. The move

Look at what actually changes from one requirement to the next.

It is never the shape "take a duration, produce a fee". That has been stable since section 1. What changes every single time is the rule for turning a stay into a number.

So give that one varying piece of behaviour a name of its own, and make everything that needs a fee depend on the name instead of the rule.

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

An interface with one method states a contract. Anything claiming to be a PricingPolicy can answer this one question, however it works the answer out inside.

Today's rule becomes the first implementation. Notice it looks like 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;
        };
    }
}

Now the classes that need a fee stop containing the rule. They hold a PricingPolicy, 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 get the 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. They have to. Neither one holds an opinion about pricing, so both ask the single object whose job that is.

A members' discount is now a second class implementing PricingPolicy, wired in wherever the lot wants it. Neither Till nor ReceiptKiosk gets edited to make room.

That is the "open for extension, closed for modification" idea from chapter 2.5, now with a shape you can actually build.

And that is the Strategy pattern. There is nothing more to it than what you 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 got.

6. What modern Java changes here

PricingPolicy has exactly one abstract method. That makes it a functional interface, the idea from chapter 1.4.

Anything with one abstract method can be satisfied by a lambda. No implements clause, no @Override.

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

That is still Strategy. The seam is the interface; the syntax you fill it with does not change what the pattern is.

So when do you pick which? Reach for a named class when the rule has a name worth reading at the call site, or when it carries its own data the way FlatHourlyPricing would with configurable rates. Reach for a lambda when the rule really is one expression with nothing to remember between calls.

One historical note worth carrying. The pattern catalogue this comes from was written in 1994, long before Java had lambdas. Learn Strategy only from the book and you will write a needless class every time 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 satisfied by exactly one class decouples nothing, because nothing is varying. This app's 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, and gets tagged over-engineered (premature interface).

One question settles it before you write the interface:

Can I name the second implementation right now, and write the one method signature both would satisfy?

Answer no, and the interface is a guess about the future. Waiting costs you nothing.

The flat-rate charge from section 2 was correct code the day it was written. 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. You never opened Till. A brand-new pricing rule, one that ignores an argument FlatHourlyPricing cared about, slotted in as a single new file.

Going deeper

A lambda strategy is a real object. It is not syntax sugar that vanishes at compile time, and it can hold values from the method that created it without declaring a single field.

Watch this directly, because it changes what a strategy can do and what a test can assume.

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. No class has them as fields.

Java lets a lambda read a local from its enclosing method on one condition: the local must be effectively final, meaning nothing reassigns it after it is set. When that holds, the lambda keeps its own private copy — a capture — which outlives the call to discounted.

That is what lets you build a whole family of related strategies from one factory method, each with its own baked-in discount, without ever writing TenPercentOffPricing or TwentyPercentOffPricing.

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

Now sit with the last line.

discounted(10) called twice gives you two lambdas that compute the identical answer for every input. They are still not equal.

Neither overrides equals, so both fall back to identity. Two different objects, identical behaviour, unequal.

This constrains how you test. 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.

So here is the rule that falls out of it. A strategy you need to compare, log meaningfully, or look up in a Set wants a named class, 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 almost every system they hand you has a 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 point at the actual second requirement that justified the interface.

Then, asked "would you always do this?", they say no — a single fixed rule with no second case in view does not need one. Say that and you have separated yourself from most of the room.


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 →