DEV Community

Machine coding Master
Machine coding Master

Posted on

Kill the Visitor Pattern: Refactoring Domain ASTs with Java 21 Sealed Hierarchies

Kill the Visitor Pattern: Refactoring Domain ASTs with Java 21 Sealed Hierarchies

For decades, the Visitor pattern was our only escape hatch for adding operations to object structures without mutating them. In modern backend engineering, clinging to double-dispatch boilerplate across your domain evaluators and AST engines is pure technical debt—sealed hierarchies and record patterns have rendered it completely obsolete.

Heads up: if you want to see these patterns applied to real interview problems, javalld.com has full machine coding solutions with traces.

Why Most Developers Get This Wrong

  • Treating the Gang of Four Visitor pattern as an untouchable standard, forcing every new domain node to implement accept(NodeVisitor v) and fragmenting traversal logic across dozens of files.
  • Reverting to manual instanceof checks with ugly if/else ladders instead of trusting the compiler to guarantee total coverage.
  • Leaving intermediate record extractors in place rather than leveraging nested pattern deconstruction to unwrap complex payloads in a single expression.

The Right Way

  • Model your domain engine as an algebraic data type with sealed interfaces, then evaluate nodes using exhaustive switch expressions and nested deconstruction.
  • Declare the root node as sealed interface Node permits ... to turn your hierarchy into a closed, compiler-enforced contract.
  • Delete all accept(), visit(), and Visitor classes across the entire codebase—zero double-dispatch overhead.
  • Match on structural shapes directly in switch arms, extracting nested values without intermediate local variables.
  • Eliminate runtime default branches: adding a new record type to the sealed hierarchy breaks the build at compile time wherever an evaluator exists.

Show Me The Code

sealed interface Expr permits Const, Add, Neg {}
record Const(double val) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Neg(Expr expr) implements Expr {}

static double eval(Expr expr) {
    return switch (expr) {
        case Const(var val) -> val;
        case Add(Const(var l), Const(var r)) -> l + r; // nested deconstruction
        case Add(var l, var r) -> eval(l) + eval(r);
        case Neg(var e) -> -eval(e);
    };
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Double-dispatch is dead: pattern matching gives you clean separation of data and behavior without recursive indirection.
  • Exhaustiveness is safer than visitor contracts: javac catches missing edge cases at compile time without relying on runtime fallbacks.
  • Nested record patterns collapse tree transformations: complex AST evaluation logic drops from hundreds of boilerplate lines down to declarative switch blocks.

Top comments (0)