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
instanceofchecks with uglyif/elseladders 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
switchexpressions 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(), andVisitorclasses 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
defaultbranches: 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);
};
}
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:
javaccatches 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)