Skip to content

How to Implement the Strategy Pattern in Java (Java 21)

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.

how-to-tutorials design-patterns java-tutorials
Gopi Gorantala
Reading Progress

On This Page

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.

--- 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.

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

The finished tree:

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

package com.example.library;

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

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

package com.example.library;

public enum ItemType { BOOK, DVD }

src/main/java/com/example/library/Loan.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

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.

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

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

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

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

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

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.

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.

src/main/java/com/example/library/FeeChecks.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");
    }
}
java -ea -cp out com.example.library.FeeChecks
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, 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:

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:

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(...).

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:

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.

Related reading on this site: how to use Java records (the Loan), how to implement the builder pattern in Java, how to create an immutable class in Java (why PerDayWithCap is safe to share), and, coming next, how to implement the decorator pattern.

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

how-to-tutorialsdesign-patternsjava-tutorials

Staff engineer architecting Kafka and Flink platforms for banking, where a dropped event is a reconciliation break, not a retry. Through javahandbook.com, I share production-tested guidance on building reliable event-driven systems.

Comments