Pattern matching with Case
Replace if/else cascades and switch ladders with declarative Case branches that each yield a Result.
May 17, 2026
Java’s three branching constructs โ if/else, the ternary ?:, and switch โ share a quiet flaw: they describe how to test, not what the branches are. They also tightly couple the order of evaluation to the source layout. Case flips this around: each branch is a self-contained pair of condition and result, and the API explicitly chooses the first one that fires.
The problem with if/else ladders
String grade;
if (score >= 90) grade = "A";
else if (score >= 80) grade = "B";
else if (score >= 70) grade = "C";
else grade = "F";
You can read this top-to-bottom only because the variable assignment is on every line. Add a fourth branch and you’d better remember to update the else. Worse, grade is mutable for the span of that block โ there’s no compile-time guarantee that every path assigns it.
The ternary form is no better:
String grade = (score >= 90) ? "A"
: (score >= 80) ? "B"
: (score >= 70) ? "C"
: "F";
Read it once, fine. Maintain it over time and you’ll be counting parens.
The Case approach
Case makes every branch a value built by matchCase(condition, result), and the default is a separate DefaultCase constructed by the no-condition matchCase(result):
import static com.svenruppert.functional.matcher.Case.match;
import static com.svenruppert.functional.matcher.Case.matchCase;
import static com.svenruppert.functional.model.Result.success;
Result<String> grade = match(
matchCase(() -> success("F")), // default
matchCase(() -> score >= 90, () -> success("A")),
matchCase(() -> score >= 80, () -> success("B")),
matchCase(() -> score >= 70, () -> success("C"))
);
grade.ifPresentOrElse(System.out::println, System.err::println);
Three properties worth noticing:
- Branches are values. You can store them, pass them, generate them โ they’re just
Supplier<Boolean>+Supplier<Result<T>>pairs. - Evaluation is explicit.
matchwalks the branches in the order you wrote them, returning the first whose condition holds. No fall-through, noelse. - Every branch yields a
Result. Even the “no branch matched” case is handled โ theDefaultCaseprovides the fallback.
Lazy conditions, lazy values
Both halves of a matchCase are Suppliers. That means:
- Conditions are not evaluated until
matchreaches them โ earlier matches short-circuit. - Results are not constructed until their branch is chosen โ expensive payloads only build for the branch you actually pick.
Result<Heavy> r = match(
matchCase(() -> success(buildDefault())),
matchCase(() -> mode == FAST, () -> success(buildFast())),
matchCase(() -> mode == ACCURATE, () -> success(buildAccurate()))
);
buildAccurate() and buildDefault() only run if their branches win.
Functions as branch values
Because each branch returns a Result<T> for any T, T can be a Function. That gives you a Case-as-strategy-selector:
Result<Function<Integer, Integer>> op = match(
matchCase(() -> success(v -> v)), // identity
matchCase(() -> input == ADD, () -> success(v -> v + 1)),
matchCase(() -> input == DOUBLE, () -> success(v -> v * 2)),
matchCase(() -> input == NEGATIVE, () -> success(v -> -v))
);
op.map(f -> f.apply(10))
.ifPresent(System.out::println);
Three lines that would be a switch returning a method reference โ but here every branch is a value, easy to test in isolation.
Failure as just another branch
The default branch can be a Failure:
Result<String> r = match(
matchCase(() -> Result.failure("no branch matched")),
matchCase(() -> input == "A", () -> success("hello A")),
matchCase(() -> input == "B", () -> success("hello B"))
);
r.ifPresentOrElse(System.out::println, err -> log.warn(err));
No exception, no sentinel value โ the absence of a match becomes a regular Result.failure your caller can handle alongside any other failure.
When not to use Case
Case shines when branches need their own predicates and produce values. Stick with:
switchfor cheap enum/int dispatch with no branch-internal logic.if/elsewhen there’s no value to produce.Map.getwhen you really have a lookup table, not a decision tree.
Recap
- Each branch is a
matchCase(condition, result)value โ composable and testable. match(default, branches...)evaluates branches in order and returns the first match.- Conditions and results are
Suppliers โ lazily evaluated. - Failure is just another
Resultโ no exceptions, no nulls.
Next
CaseAPI reference.- Composing functions โ once branch values are functions, you’ll want to combine them.
- Error handling with
Resultโ what every branch returns.