LLD Dojo

Strategy

Core. Expect to meet this one, and expect to be asked for it by name.

Start with the problem

A parking lot needs to charge for a stay. The first version writes itself.

final class Till {
    long charge(Duration stayed) {
        long hours = Math.max(1, (stayed.toMinutes() + 59) / 60);
        return hours * 5000;          // 50 rupees an hour
    }
}

For one afternoon this is correct, and there is nothing wrong with writing it. It calculates a fee, and a reviewer would understand it immediately.

Then a second requirement arrives. Motorbikes are cheaper than cars.

The obvious response is a conditional. You pass the vehicle type in, and you branch on it.

long charge(Duration stayed, VehicleType type) {
    long hours = Math.max(1, (stayed.toMinutes() + 59) / 60);
    if (type == VehicleType.MOTORBIKE) return hours * 2000;
    return hours * 5000;
}

This also works. It is worth being honest that at two cases, this is still a reasonable piece of code.

Watch where it goes

Now add a third requirement: an overnight flat rate. Then a fourth: members get a discount. Each one adds a branch to the same method, and the method starts to hold every pricing rule the business has ever asked for.

Two costs appear, and the second is the one that matters.

The first cost is that charge grows. That is annoying but survivable.

The second cost is that someone else needs a fee. A receipt printer wants to show the amount before the driver leaves. Because the rule lives inside Till.charge, the receipt printer either calls into Till for something that is not its job, or it copies the branches. In practice, people copy. Now the rule exists in two places, and the next change updates only one of them.

That second cost is the real reason to act, and it is worth saying out loud in an interview.

The move

The fee calculation is the part that varies. Everything else about Till stays the same. So give the varying part a name of its own.

interface PricingPolicy {
    long feeMinor(Duration stayed);
}

Then let Till receive one instead of containing the rule.

final class Till {
    private final PricingPolicy pricing;

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

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

Each rule now becomes its own small class.

final class FlatHourly implements PricingPolicy {
    private final long perHourMinor;

    FlatHourly(long perHourMinor) {
        this.perHourMinor = perHourMinor;
    }

    @Override public long feeMinor(Duration stayed) {
        long hours = Math.max(1, (stayed.toMinutes() + 59) / 60);
        return hours * perHourMinor;
    }
}

That is the Strategy pattern. There is nothing more to it than the three pieces you have just seen: an interface for the varying decision, implementations that each know one rule, and a holder that calls through the interface without knowing which implementation it has.

The payoff is that the fifth pricing rule is a new file, and Till is not edited at all. The receipt printer can now be handed the same policy object, so there is one rule in one place.

What modern Java changes here

PricingPolicy has a single abstract method, which makes it a functional interface. So an implementation can be a lambda rather than a class.

Till freeToday = new Till(stayed -> 0L);

This is still Strategy. The pattern is the seam, and the seam is the interface. Whether a particular implementation arrives as a named class or as a lambda is a separate, smaller decision.

Prefer a named class when the rule has a name worth reading at the call site, or when it needs state of its own, as FlatHourly needs its rate. Prefer a lambda when the rule really is one expression.

A pattern catalogue written in 1994 cannot tell you this, which is why patterns learned only from the book tend to come out heavier than a Java reviewer expects.

When naming it is wrong

One implementation, with no second one in sight, is not Strategy. It is a field with extra steps.

The Standard here penalises that as hard as it penalises having no seam at all. Dimension D3 asks for the seam set to be minimal, so a speculative interface with one implementation scores lower than the plain code it replaced. The failure tag is over-engineered (premature interface).

One question settles it. Can you name the second implementation right now, and write the single signature that both of them satisfy? If you cannot, the interface is a guess, and waiting costs you nothing.

Where this lives in the app

Syllabus item B1 is the full lesson, with a faded exercise and a measured contrast pair that shows the seam paying off once and then costing more than it saved. corpus/parking-lot builds on it.

All reference pages