Fundamentals 15 min read

How Pattern Matching Let Me Scrap the Visitor Pattern in Java

The article walks through a real‑world refactor of a Java event‑handling system, showing how instanceof pattern matching, switch pattern matching, record patterns, sealed classes and the new JDK 25 syntax features replace verbose if‑else chains and the Visitor pattern, yielding shorter, safer, compile‑time‑checked code.

Tinker Programmer
Tinker Programmer
Tinker Programmer
How Pattern Matching Let Me Scrap the Visitor Pattern in Java

Why the original code failed

In a three‑year‑old event‑center, each event type was handled with a long if‑else if chain that relied on instanceof checks and manual casts. Adding a new event (e.g., OrderRefundEvent) without updating the chain caused messages to be dropped into the dead‑letter queue, exposing three main problems:

Every branch required an instanceof test followed by a cast, making the code noisy.

The temporary variable name (usually e) was reused, leading to confusion.

The compiler could not verify that all event sub‑types were covered, so a missing branch went unnoticed.

Long else if chains reduced readability.

Instanceof pattern matching (Java 16)

Java 16 (JEP 394) introduced pattern variables, allowing a cast to be combined with the type test:

// before
if (event instanceof OrderCreateEvent) {
    OrderCreateEvent e = (OrderCreateEvent) event;
    orderService.create(e.getOrderId());
}

// after
if (event instanceof OrderCreateEvent e) {
    orderService.create(e.getOrderId());
}

The pattern variable e exists only inside the if block, preventing scope leakage. A pitfall is mixing the pattern with logical ||; the compiler rejects it because the right‑hand side might reference an undefined variable. Only && can be combined safely.

Guarded clauses

When additional conditions are needed, a guard can be added:

if (event instanceof OrderCreateEvent e && e.getAmount() > 100) {
    // e is usable here
}

Switch pattern matching (Java 21)

Java 21 (JEP 441) merges switch expressions with pattern matching, turning the previous 200‑line chain into a concise switch:

public void handle(Event event) {
    switch (event) {
        case OrderCreateEvent e -> orderService.create(e.getOrderId());
        case OrderPayEvent e    -> payService.confirm(e.getOrderId(), e.getAmount());
        case UserRegisterEvent e-> userService.welcome(e.getUserId());
        case OrderRefundEvent e -> refundService.process(e.getOrderId());
        case null               -> log.warn("Received null message, discarding");
        default                 -> log.warn("Unhandled event type: {}", event.getClass());
    }
}

Guarded clauses can filter by condition:

case OrderCreateEvent e when e.getAmount() > 10000 -> orderService.createVipOrder(e);
case OrderCreateEvent e -> orderService.createNormalOrder(e);

Guarded cases must appear before the unguarded case of the same type, mirroring ordinary switch ordering.

Exhaustiveness with sealed classes

If Event is a sealed interface listing all permitted sub‑types, the compiler forces the switch to cover every case, eliminating the risk of forgotten branches:

public sealed interface Event permits OrderCreateEvent, OrderPayEvent, UserRegisterEvent, OrderRefundEvent {}

Record patterns (Java 21)

When events are defined as records, the switch can deconstruct fields directly, removing the need for getters:

case OrderCreateEvent(Long orderId, Long userId, BigDecimal amount) ->
    orderService.create(orderId, userId, amount);

Nested records can be deconstructed layer by layer, and unused fields can be ignored with the underscore placeholder _.

Unnamed variables (Java 22)

Java 22 (JEP 456) allows the underscore to represent an ignored variable, simplifying code where only a subset of fields is needed:

case OrderCreateEvent(Long orderId, _, _) -> orderService.create(orderId);
map.forEach((_, value) -> System.out.println(value));

Note that the underscore can no longer be used as a regular variable name; it is strictly an unnamed placeholder.

JDK 25 syntax sugar

Super‑call statements : Code can now appear before super() for argument validation.

Module import : import module java.base; imports all exported packages of a module.

Compact source files : A file can contain a top‑level void main() method without an explicit class, and the file name no longer needs to match a class name.

Replacing the Visitor pattern

Previously, an expression evaluator required a visitor interface, multiple visit methods, and an accept method on each node. With sealed interfaces and pattern matching, the whole visitor infrastructure disappears:

public sealed interface Expr permits NumberExpr, AddExpr, MultiplyExpr {}
public record NumberExpr(int value) implements Expr {}
public record AddExpr(Expr left, Expr right) implements Expr {}
public record MultiplyExpr(Expr left, Expr right) implements Expr {}

public int eval(Expr expr) {
    return switch (expr) {
        case NumberExpr(var v)          -> v;
        case AddExpr(var l, var r)      -> eval(l) + eval(r);
        case MultiplyExpr(var l, var r) -> eval(l) * eval(r);
    };
}

The method shrank from dozens of lines to ten, and any new expression type forces a compile‑time error until handled.

Takeaways

Instanceof pattern matching removes manual casts and limits variable scope.

Switch pattern matching with sealed classes provides exhaustive, readable type dispatch.

Record patterns deconstruct objects without getters; nested records are supported.

Unnamed variables let you discard irrelevant fields cleanly.

JDK 25 introduces small but useful syntax improvements for constructors, imports, and main methods.

Applying these features turned a brittle 200‑line event handler into a 40‑line, compile‑time‑checked implementation, eliminating the “forgotten branch” bugs that previously caused data loss.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javaPattern MatchingSealed ClassesSwitchRecordsVisitor PatternJDK 25
Tinker Programmer
Written by

Tinker Programmer

Solving problems with code, sharing practical tech insights, and leveling up together!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.