> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Implement the Strategy Pattern in Java (Java 21)
- URL: https://www.ggorantala.dev/how-to-implement-strategy-pattern-java/
- Published: 2026-01-17T06:00:00.000Z
- Updated: 2026-10-31T06:35:31.000Z
- Description: Replace a growing if/else chain with the strategy pattern in Java 21. A library late-fee calculator, built step by step, with lambdas, an EnumMap and 15 checks.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, design-patterns, java-tutorials

## What you'll have when you're done

This is the output of the program you'll write. Seven loans go through a new, strategy-based fee service, and each result is compared with the old `if/else` method it replaces. At the bottom, one rule gets swapped while the program is running.

```log
--- strategy vs old if/else chain ---
Dune             BOOK STANDARD 12 days late  fee   3.00  matches old code: true
Dune             BOOK STANDARD 90 days late  fee  10.00  matches old code: true
Alien            DVD  STANDARD  4 days late  fee   4.00  matches old code: true
Matilda          BOOK CHILD    30 days late  fee   0.00  matches old code: true
Middlemarch      BOOK SENIOR   10 days late  fee   0.70  matches old code: true
Middlemarch      BOOK SENIOR    2 days late  fee   0.00  matches old code: true
Cataloguing 101  BOOK STAFF     8 days late  fee   0.00  matches old code: true
--- swap a rule at runtime ---
before: 3.00
flat-fee trial: 2.00

```

The last column says `true` on every row, so the new code charges exactly what the old code did. What's different is where the rules live. In the old version they're branches in one method. In the new one each rule is its own small object, and adding a rule means adding a class instead of editing a method that every other rule depends on.

If you want to know how to implement the strategy pattern in Java without a framework, a UML diagram or a `Context` class with a name nobody remembers, this is the version I'd hand a colleague. The example is a library working out late fees: books cost 25p a day, DVDs £1, seniors get a grace period, children never pay. Money rules like these change every year, which is what makes the pattern earn its keep.

## Tools and versions

| Tool       | Version I used                   | Check it with  |
| ---------- | -------------------------------- | -------------- |
| JDK        | OpenJDK 21.0.12.1 (Ubuntu build) | java -version  |
| javac      | 21.0.12.1                        | javac -version |
| Build tool | none, plain javac                |                |

No dependencies. The code uses a record, a switch expression and `EnumMap`, so Java 21 is what I tested, and it should compile on Java 17 as well (switch expressions arrived in 14, records in 16). I only ran it on 21.

I ran everything from the command line. The IntelliJ IDEA and Eclipse steps below come from their documentation and I didn't click through them. Maven Central wasn't reachable from my build machine, so there's no JUnit here either; the checks are a plain `main` method you can paste into JUnit tests later. The build took me about 35 minutes, and the longest part was deciding whether the settings belong in the strategy or in the loan. More on that in a moment.

You should know what an interface and an `enum` are. You don't need to have met the pattern before.

## Set up the library project

I'm using the command line as the primary path this time, with the project laid out the way an IDE would.

```bash
mkdir -p library/src/main/java/com/example/library
cd library

```

The finished tree:

```log
library
└── src
    └── main
        └── java
            └── com
                └── example
                    └── library
                        ├── App.java
                        ├── FeeCalculatorBefore.java
                        ├── FeeChecks.java
                        ├── FeePolicies.java
                        ├── FeePolicy.java
                        ├── FeeService.java
                        ├── ItemType.java
                        ├── Loan.java
                        ├── Membership.java
                        └── PerDayWithCap.java

```

### In IntelliJ IDEA

File → New → Project, pick Java as the generator, choose a JDK 21, and leave the build system on IntelliJ. Right-click `src` → New → Package, type `com.example.library`, then create each class below with New → Java Class. For an interface or enum, change the Kind in that same dialog. To run, click the green arrow next to `main`.

### In Eclipse

File → New → Java Project, select the JavaSE-21 execution environment, and create the package under `src`. Use File → New → Class, Interface or Enum as each file calls for. Run with Run As → Java Application.

### With Maven or Gradle

There's nothing to configure. Generate a skeleton with `mvn archetype:generate` or `gradle init`, set the Java version to 21, and put the files under `src/main/java`.

## Replace the if/else chain with the strategy pattern, step by step

Two words first. A **strategy** is an object that knows how to do one job one way (here, work out a fee). The **context** is the object that has a job to do and picks which strategy does it. That's the entire pattern. Everything below is how to wire it up so it stays pleasant to change.

### Step 1: Write the code you're about to replace

Goal: have a baseline to compare against.

`src/main/java/com/example/library/Membership.java`

```java
package com.example.library;

public enum Membership { STANDARD, CHILD, SENIOR, STAFF }

```

`src/main/java/com/example/library/ItemType.java`

```java
package com.example.library;

public enum ItemType { BOOK, DVD }

```

`src/main/java/com/example/library/Loan.java`

```java
package com.example.library;

/** One borrowed item that came back late (or on time, when daysLate is 0). */
public record Loan(String title, ItemType itemType, Membership membership, int daysLate) {
    public Loan {
        if (daysLate < 0) {
            throw new IllegalArgumentException("daysLate must be 0 or more, got " + daysLate);
        }
    }
}

```

The record rejects negative days in its constructor, so no strategy ever has to check for it.

`src/main/java/com/example/library/FeeCalculatorBefore.java`

```java
package com.example.library;

import java.math.BigDecimal;

/** The version most of us write first: one method, one growing if/else chain. */
public class FeeCalculatorBefore {

    public BigDecimal fee(Loan loan) {
        if (loan.daysLate() == 0) {
            return BigDecimal.ZERO;
        }
        if (loan.membership() == Membership.CHILD || loan.membership() == Membership.STAFF) {
            return BigDecimal.ZERO;
        } else if (loan.membership() == Membership.SENIOR) {
            int chargeable = Math.max(0, loan.daysLate() - 3);          // 3 days grace
            return new BigDecimal("0.10").multiply(BigDecimal.valueOf(chargeable))
                    .min(new BigDecimal("5.00"));
        } else {
            BigDecimal perDay = loan.itemType() == ItemType.DVD ? new BigDecimal("1.00") : new BigDecimal("0.25");
            BigDecimal cap = loan.itemType() == ItemType.DVD ? new BigDecimal("15.00") : new BigDecimal("10.00");
            return perDay.multiply(BigDecimal.valueOf(loan.daysLate())).min(cap);
        }
    }
}

```

This is the shape I see in most codebases. It works, and four months from now it has seven branches, two of them nested, and every change risks the rest. Note the quiet default in the `else`: any membership type that isn't child, staff or senior is charged as standard. Hold that thought until the troubleshooting section.

Checkpoint: compile.

```bash
javac -d out $(find src -name '*.java')

```

This compiles every file in the folder, and the later steps reference classes you haven't written yet, so hold off until Step 6 and compile everything then. Or compile only what you have so far: `javac -d out Membership.java ItemType.java Loan.java FeeCalculatorBefore.java`, run from the package folder. On Windows PowerShell, list the files by hand, because `find` isn't there.

### Step 2: Define the strategy interface

Goal: give every fee rule the same shape.

`src/main/java/com/example/library/FeePolicy.java`

```java
package com.example.library;

import java.math.BigDecimal;

/** The strategy: one question, "what does this loan cost?". */
@FunctionalInterface
public interface FeePolicy {
    BigDecimal feeFor(Loan loan);
}

```

One method, which makes it a functional interface. That's deliberate: the simplest rules can then be a lambda, with no class at all. `@FunctionalInterface` isn't required, but it makes the compiler complain if someone adds a second method and breaks every lambda.

Notice the method takes the whole `Loan`, not a bare number of days. Different rules need different inputs (the item type, the membership), and passing the loan means you never change the interface when a new rule wants one more fact.

### Step 3: Write a strategy that has settings

Goal: one class that covers book fees, DVD fees and the senior rule by changing numbers.

`src/main/java/com/example/library/PerDayWithCap.java`

```java
package com.example.library;

import java.math.BigDecimal;
import java.util.Objects;

/** A strategy with its own settings: pence per day, up to a cap, after a grace period. */
public final class PerDayWithCap implements FeePolicy {

    private final BigDecimal perDay;
    private final BigDecimal cap;
    private final int graceDays;

    public PerDayWithCap(BigDecimal perDay, BigDecimal cap, int graceDays) {
        this.perDay = Objects.requireNonNull(perDay, "perDay");
        this.cap = Objects.requireNonNull(cap, "cap");
        if (graceDays < 0) throw new IllegalArgumentException("graceDays must be 0 or more");
        this.graceDays = graceDays;
    }

    @Override
    public BigDecimal feeFor(Loan loan) {
        int chargeable = Math.max(0, loan.daysLate() - graceDays);
        return perDay.multiply(BigDecimal.valueOf(chargeable)).min(cap);
    }
}

```

This was the decision I mentioned. My first draft had three classes: `BookFee`, `DvdFee` and `SeniorFee`, each with the arithmetic copied in. They differed only by three numbers. A strategy can take its settings through its constructor, so one class with three instances replaced three classes. I'd start there rather than at one class per rule.

The fields are `final` and set in the constructor, so a single instance is safe to share between threads and between callers. That matters in Step 5.

### Step 4: Create the ready-made strategies

Goal: one place to look up every rule the library has.

`src/main/java/com/example/library/FeePolicies.java`

```java
package com.example.library;

import java.math.BigDecimal;

/** Ready-made strategies. Short ones are lambdas; ones with settings are classes. */
public final class FeePolicies {

    private FeePolicies() {}

    public static final FeePolicy NO_FEE = loan -> BigDecimal.ZERO.setScale(2);

    public static final FeePolicy BOOK = new PerDayWithCap(new BigDecimal("0.25"), new BigDecimal("10.00"), 0);

    public static final FeePolicy DVD = new PerDayWithCap(new BigDecimal("1.00"), new BigDecimal("15.00"), 0);

    public static final FeePolicy SENIOR = new PerDayWithCap(new BigDecimal("0.10"), new BigDecimal("5.00"), 3);

    /** Picks BOOK or DVD from the item type. A strategy can delegate to other strategies. */
    public static final FeePolicy BY_ITEM = loan -> switch (loan.itemType()) {
        case BOOK -> BOOK.feeFor(loan);
        case DVD -> DVD.feeFor(loan);
    };
}

```

Three things are going on:

- `NO_FEE` is a lambda. It has no settings and one line of logic, so a class would be noise. The `setScale(2)` makes it print `0.00` like the others; I skipped that on my first run and the output read `0` on the free rows.
- `BOOK`, `DVD` and `SENIOR` are instances of the class from Step 3.
- `BY_ITEM` is a strategy that picks another strategy. Standard members pay by item type, so this one delegates. The `switch` has no `default` on purpose, and the first snag below shows why.

### Step 5: Write the context that chooses

Goal: a service that maps a membership to a strategy and nothing else.

`src/main/java/com/example/library/FeeService.java`

```java
package com.example.library;

import java.math.BigDecimal;
import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;

/** The context: knows which strategy applies to whom, never how a fee is worked out. */
public class FeeService {

    private final Map<Membership, FeePolicy> policies = new EnumMap<>(Membership.class);

    public FeeService() {
        policies.put(Membership.STANDARD, FeePolicies.BY_ITEM);
        policies.put(Membership.CHILD, FeePolicies.NO_FEE);
        policies.put(Membership.SENIOR, FeePolicies.SENIOR);
        policies.put(Membership.STAFF, FeePolicies.NO_FEE);
    }

    /** Swap the rule for one membership type, for example from configuration. */
    public void use(Membership membership, FeePolicy policy) {
        policies.put(Objects.requireNonNull(membership), Objects.requireNonNull(policy));
    }

    public BigDecimal feeFor(Loan loan) {
        FeePolicy policy = policies.get(loan.membership());
        if (policy == null) {
            throw new IllegalStateException("No fee policy registered for " + loan.membership());
        }
        return policy.feeFor(loan);
    }
}

```

`FeeService` is the context. It never computes a fee. It looks up the policy for `loan.membership()` and asks it. The `EnumMap` is a good fit when the key is an enum: it's compact, it iterates in the enum's declared order, and it can't hold a key of the wrong type.

`use(...)` is how you swap a rule at runtime, from configuration or a feature flag. Both arguments are null-checked, so a bad swap fails at the call that caused it, not at some later `feeFor` call.

`feeFor` throws if a membership has no policy. I prefer that to the old `else` that quietly charged people.

One thing to watch: `EnumMap` isn't thread-safe. If you register policies at start-up and only read afterwards, that's fine. If several threads call `use(...)` while others read, wrap the map with `Collections.synchronizedMap` or switch to a `ConcurrentHashMap`. The strategies themselves are immutable, so they're safe either way.

### Step 6: Write the demo

Goal: run the new service against the old calculator and see the same answers.

`src/main/java/com/example/library/App.java`

```java
package com.example.library;

import java.math.BigDecimal;
import java.util.List;

public class App {

    public static void main(String[] args) {
        List<Loan> loans = List.of(
                new Loan("Dune", ItemType.BOOK, Membership.STANDARD, 12),
                new Loan("Dune", ItemType.BOOK, Membership.STANDARD, 90),
                new Loan("Alien", ItemType.DVD, Membership.STANDARD, 4),
                new Loan("Matilda", ItemType.BOOK, Membership.CHILD, 30),
                new Loan("Middlemarch", ItemType.BOOK, Membership.SENIOR, 10),
                new Loan("Middlemarch", ItemType.BOOK, Membership.SENIOR, 2),
                new Loan("Cataloguing 101", ItemType.BOOK, Membership.STAFF, 8));

        FeeService service = new FeeService();
        FeeCalculatorBefore before = new FeeCalculatorBefore();

        System.out.println("--- strategy vs old if/else chain ---");
        for (Loan loan : loans) {
            BigDecimal fee = service.feeFor(loan);
            boolean same = fee.compareTo(before.fee(loan)) == 0;
            System.out.printf("%-16s %-4s %-8s %2d days late  fee %6s  matches old code: %s%n",
                    loan.title(), loan.itemType(), loan.membership(), loan.daysLate(), fee, same);
        }

        System.out.println("--- swap a rule at runtime ---");
        Loan sample = new Loan("Dune", ItemType.BOOK, Membership.STANDARD, 12);
        System.out.println("before: " + service.feeFor(sample));
        service.use(Membership.STANDARD, loan -> new BigDecimal("2.00"));   // flat-fee trial
        System.out.println("flat-fee trial: " + service.feeFor(sample));
    }
}

```

Checkpoint: compile and run.

```bash
javac -d out $(find src -name '*.java')
java -cp out com.example.library.App

```

You should see the block from the top of this article. If every row ends in `true`, the hard part's done.

## See it working

The two parts of that output do different jobs. The first block proves the refactor didn't change any behaviour. The second proves the point of the pattern: `service.use(Membership.STANDARD, loan -> new BigDecimal("2.00"))` replaced the standard rule with a flat-fee trial using a one-line lambda, and `FeeService` didn't change at all. Neither did `PerDayWithCap`, `FeePolicies` or the loan.

## Write the checks

Without a download, I used a plain `main` method with `-ea`. In your own project turn each `check` call into a JUnit 5 test; I'll cover that in [how to write unit tests with JUnit 5](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev).

`src/main/java/com/example/library/FeeChecks.java`

```java
package com.example.library;

import java.math.BigDecimal;

/** Plain checks, run with java -ea. No test framework: nothing to download. */
public class FeeChecks {

    static int passed = 0;

    static void check(String name, boolean ok) {
        if (!ok) throw new AssertionError("FAILED: " + name);
        System.out.println("ok   " + name);
        passed++;
    }

    static Loan loan(ItemType t, Membership m, int days) {
        return new Loan("x", t, m, days);
    }

    static boolean fee(FeePolicy p, Loan l, String expected) {
        return p.feeFor(l).compareTo(new BigDecimal(expected)) == 0;
    }

    public static void main(String[] args) {
        FeeService service = new FeeService();

        check("book: 12 days late costs 3.00",
                service.feeFor(loan(ItemType.BOOK, Membership.STANDARD, 12)).compareTo(new BigDecimal("3.00")) == 0);
        check("book fee is capped at 10.00", fee(FeePolicies.BOOK, loan(ItemType.BOOK, Membership.STANDARD, 500), "10.00"));
        check("dvd: 4 days late costs 4.00", fee(FeePolicies.DVD, loan(ItemType.DVD, Membership.STANDARD, 4), "4.00"));
        check("dvd fee is capped at 15.00", fee(FeePolicies.DVD, loan(ItemType.DVD, Membership.STANDARD, 99), "15.00"));
        check("on time costs nothing", fee(FeePolicies.BY_ITEM, loan(ItemType.BOOK, Membership.STANDARD, 0), "0.00"));
        check("senior: grace period of 3 days", fee(FeePolicies.SENIOR, loan(ItemType.BOOK, Membership.SENIOR, 3), "0.00"));
        check("senior: day 4 is the first charged day", fee(FeePolicies.SENIOR, loan(ItemType.BOOK, Membership.SENIOR, 4), "0.10"));
        check("senior fee is capped at 5.00", fee(FeePolicies.SENIOR, loan(ItemType.BOOK, Membership.SENIOR, 365), "5.00"));
        check("child never pays", fee(FeePolicies.NO_FEE, loan(ItemType.DVD, Membership.CHILD, 40), "0.00"));
        check("service: child never pays",
                service.feeFor(loan(ItemType.DVD, Membership.CHILD, 40)).compareTo(BigDecimal.ZERO) == 0);

        // every membership has a policy, so a new enum constant that nobody registers is caught here
        for (Membership m : Membership.values()) {
            service.feeFor(loan(ItemType.BOOK, m, 1));
        }
        check("every membership type has a policy", true);

        service.use(Membership.STANDARD, l -> new BigDecimal("2.00"));
        check("policy can be swapped at runtime",
                service.feeFor(loan(ItemType.BOOK, Membership.STANDARD, 12)).compareTo(new BigDecimal("2.00")) == 0);

        // strategies compared with the old if/else chain over a grid of inputs
        FeeService fresh = new FeeService();
        FeeCalculatorBefore old = new FeeCalculatorBefore();
        int compared = 0;
        for (ItemType t : ItemType.values())
            for (Membership m : Membership.values())
                for (int d = 0; d <= 120; d++) {
                    Loan l = loan(t, m, d);
                    if (fresh.feeFor(l).compareTo(old.fee(l)) != 0) throw new AssertionError("mismatch for " + l);
                    compared++;
                }
        check("matches the old if/else chain on " + compared + " combinations", true);

        try {
            new Loan("x", ItemType.BOOK, Membership.STANDARD, -1);
            check("negative days rejected", false);
        } catch (IllegalArgumentException e) {
            check("negative days rejected: " + e.getMessage(), true);
        }
        try {
            fresh.use(Membership.CHILD, null);
            check("null policy rejected", false);
        } catch (NullPointerException e) {
            check("null policy rejected", true);
        }
        System.out.println(passed + " checks passed");
    }
}

```

```bash
java -ea -cp out com.example.library.FeeChecks

```

```log
ok   book: 12 days late costs 3.00
ok   book fee is capped at 10.00
ok   dvd: 4 days late costs 4.00
ok   dvd fee is capped at 15.00
ok   on time costs nothing
ok   senior: grace period of 3 days
ok   senior: day 4 is the first charged day
ok   senior fee is capped at 5.00
ok   child never pays
ok   service: child never pays
ok   every membership type has a policy
ok   policy can be swapped at runtime
ok   matches the old if/else chain on 968 combinations
ok   negative days rejected: daysLate must be 0 or more, got -1
ok   null policy rejected
15 checks passed

```

The grid check is the one I'd keep. It compares the new service with the old calculator on 968 combinations (2 item types × 4 memberships × 121 values of `daysLate`), so any rule I copied across wrongly would have shown up as a mismatch. Delete `FeeCalculatorBefore` once you trust that check, and not before.

## What's happening behind each step

The service holds a map from membership to `FeePolicy`. When you call `feeFor`, it does one map lookup and one interface call. There's no reflection and no framework. The JVM calls `feeFor` on whatever object is in the map, and because the interface has a single implementation per object, the call is cheap. I timed a million calls to the service and it finished in about 30 ms on this machine, which means you'd have to be doing something unusual before the pattern shows up in a profile.

The pattern earns its place when the rules change at different times and for different reasons. Library fees do: seniors' grace periods get negotiated, DVD rates follow the budget. Each change lands in one small class or one constant, and the existing checks tell you if you broke something else. If you have two rules that never change, an `if` is fine. I wouldn't reach for the pattern until a third branch appears.

A lambda is a strategy too. `FeePolicy` has one abstract method, so the compiler turns `loan -> BigDecimal.ZERO` into an object that implements it. Use a lambda when there are no settings and the logic fits on a line; use a class when it has settings, a name worth documenting, or tests of its own. Java's own `Comparator` is the strategy pattern in the standard library: `Collections.sort(list, comparator)` is a context handed a strategy. If you've [implemented a Comparator](https://ggorantala.dev/how-to-implement-comparable-comparator-java/?ref=ggorantala.dev), you've already used one.

The strategy pattern is cousin to other shapes you may be tempted to confuse it with. The template method pattern fixes an algorithm's steps in a base class and lets subclasses fill some in, using inheritance. The strategy pattern hands the whole step to another object, using composition. I'd choose composition whenever I can: you can swap it at runtime, and you can't swap a superclass.

## Snags I hit while building this

I forced each of these to get the exact text.

**A new enum constant that no switch handles.** I added `AUDIOBOOK` to `ItemType` without touching `FeePolicies`:

```log
src/main/java/com/example/library/FeePolicies.java:19: error: the switch expression does not cover all possible input values
    public static final FeePolicy BY_ITEM = loan -> switch (loan.itemType()) {
                                                    ^
1 error

```

This is why `BY_ITEM` has no `default`. A switch *expression* over an enum must cover every constant, so the compiler finds every place you forgot. Add a `default -> ...` and the error disappears, and so does your safety net.

**A new membership that nobody registered.** `Membership` is used as a map key, so the compiler can't help here. I added `VOLUNTEER` and tried a 10-day-late book:

```log
old chain: 2.50
java.lang.IllegalStateException: No fee policy registered for VOLUNTEER

```

The old code charged a volunteer £2.50 because the `else` caught everyone. The new code refuses. That's the behaviour I want, and the check "every membership type has a policy" in `FeeChecks` turns it into a failing check before it turns into a support call.

**Passing `null` to `use(...)`.**

```log
Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:233)
	at com.example.library.FeeService.use(FeeService.java:22)
	at U.main(U.java:2)

```

The cause is a caller handing over a missing policy, and the stack trace names the culprit. Without the `requireNonNull`, `EnumMap` would store the null and the failure would show up much later inside `feeFor`.

**Fees printing as `0` instead of `0.00`.** `BigDecimal.ZERO` has scale 0\. It compares equal to `0.00` with `compareTo` but not with `equals`, and it prints differently. Set the scale in the strategy so every result looks the same. Compare money with `compareTo`, never `equals`.

**Compiling with an older Java release.** I ran `javac --release 11` to see what a Java 11 project would say:

```log
Loan.java:4: error: records are not supported in -source 11
public record Loan(String title, ItemType itemType, Membership membership, int daysLate) {
       ^
  (use -source 16 or higher to enable records)

```

Check `javac -version`, and in your IDE check the project SDK and language level. In Maven, set `maven.compiler.release` to 21.

## Taking it to production

- **Where the numbers come from.** The rates are constants here. In a real service, build the `PerDayWithCap` instances from configuration, not from literals, and keep the fee tables out of the code. Money in config is a decision you'll want an audit trail for.
- **Dependency injection.** In Spring, each strategy is a bean and the context takes `Map<String, FeePolicy>` by constructor injection, which Spring fills with every bean of that type keyed by bean name. That's a good fit once you have more than a handful of rules.
- **Naming.** I used `FeePolicy` and `FeeService`. You'll see `Strategy` and `Context` in books; real code reads better with names from the domain.
- **Version note.** Before Java 8, every strategy was an anonymous class, and before Java 14 the `BY_ITEM` switch was an `if` chain with a `default`. Both still compile, but you lose the exhaustive check.
- **When not to use it.** One rule, or rules that change together, don't need a strategy. A sealed interface with a `switch` over its permitted types is a better fit when the set of rules is closed and you want the compiler to enforce it; see [how to use sealed classes](https://ggorantala.dev/how-to-use-sealed-classes-interfaces-java/?ref=ggorantala.dev).

Related reading on this site: [how to use Java records](https://ggorantala.dev/how-to-use-java-records/?ref=ggorantala.dev) (the `Loan`), [how to implement the builder pattern in Java](https://ggorantala.dev/how-to-implement-builder-pattern-java/?ref=ggorantala.dev), [how to create an immutable class in Java](https://ggorantala.dev/how-to-create-immutable-class-java/?ref=ggorantala.dev) (why `PerDayWithCap` is safe to share), and, coming next, [how to implement the decorator pattern](https://ggorantala.dev/how-to-implement-decorator-pattern-java/?ref=ggorantala.dev).

## Checklist

- Wrote the old `if/else` calculator and kept it as a baseline.
- Defined a one-method `FeePolicy` that takes the whole `Loan`.
- Put settings in a strategy's constructor instead of copying classes.
- Used lambdas for rules with no settings and classes for rules with settings.
- Kept the context free of fee arithmetic and made missing policies fail loudly.
- Proved the new code matches the old on 968 combinations, then deleted the old one.

Rules worth remembering: no `default` on a switch over your own enum, immutable strategies, and fail fast when nothing is registered.

## FAQ: strategy pattern questions

**What is the strategy pattern in Java?**  
It's a way of putting interchangeable rules behind one interface so the calling code can pick one at runtime. Here, `FeePolicy` is the interface and `FeeService` picks the rule for each loan.

**Should I use a lambda or a class for a strategy?**  
A lambda for a rule that has no settings and fits on a line. A class when it needs settings, a documented name or its own tests. Both implement the same interface, so you can mix them freely.

**How do I choose a strategy at runtime?**  
Keep a map from a key to a strategy, as `FeeService` does with an `EnumMap`, and look it up when a request comes in. For string keys, use a `HashMap` and fail with a clear exception when the key is missing.

**How is the strategy pattern different from the state pattern?**  
A strategy is chosen by the caller and usually stays the same for the object's life. A state object changes itself as the object moves through its lifecycle. The class diagrams look the same; the intent differs.

## References

- [Functional interfaces, lambdas and Comparator, Java SE 21 API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Comparator.html?ref=ggorantala.dev)
- [EnumMap, Java SE 21 API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/EnumMap.html?ref=ggorantala.dev)
- [BigDecimal, Java SE 21 API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/math/BigDecimal.html?ref=ggorantala.dev)
- [JEP 361: Switch Expressions](https://openjdk.org/jeps/361?ref=ggorantala.dev) and [JEP 395: Records](https://openjdk.org/jeps/395?ref=ggorantala.dev)