Visitor lets you add new operations over a set of types without modifying those types. It is the most intricate pattern in the catalogue, the one most often applied where something simpler would do, and the one whose motivation — the expression problem — is genuinely worth understanding.
The trade it makes
Every design that mixes types and operations must choose which is easy to add.
Diagram, as text
- Which changes more often: the types or the operations? leads to Polymorphism method on each type (new types often)
- Which changes more often: the types or the operations? leads to Visitor one class per operation (new operations often)
- Polymorphism method on each type leads to Adding a type: one new class
- Polymorphism method on each type leads to Adding an operation: edit EVERY type
- Visitor one class per operation leads to Adding an operation: one new class
- Visitor one class per operation leads to Adding a type: edit EVERY visitor
This is the expression problem, and it has no free answer in a conventional object-oriented language. Polymorphism makes types cheap and operations expensive. Visitor inverts it. Choosing wrongly means every change fights the structure.
Visitor is right when the type set is closed and the operations keep coming. An abstract syntax tree is the canonical case: the node types are fixed by the language grammar, while type-checking, optimisation, formatting, linting and code generation are all new operations over the same nodes.
Double dispatch
The mechanism is the part that confuses people. A single method call dispatches on one type — the receiver. Visitor needs two: which node, and which operation.
// Node side: each type knows only how to announce itself
class NumberLiteral {
accept(visitor) { return visitor.visitNumber(this); }
}
class Addition {
accept(visitor) { return visitor.visitAddition(this); }
}
// Operation side: one class per operation, one method per type
class Evaluate {
visitNumber(n) { return n.value; }
visitAddition(a) { return a.left.accept(this) + a.right.accept(this); }
}
class ToSource {
visitNumber(n) { return String(n.value); }
visitAddition(a) { return `(${a.left.accept(this)} + ${a.right.accept(this)})`; }
}node.accept(visitor) resolves the node type; visitor.visitAddition(this) resolves the operation. Two dispatches, and adding Optimise requires touching no node class at all.
The accept methods are pure ceremony and exist only because the language cannot dispatch on two types at once. In languages with pattern matching over closed unions, the same effect is a single match expression with no boilerplate — which is why Visitor is far less common in those languages, and why its presence in a codebase often says more about the language than the design.
Where it genuinely earns its place
Compilers and interpreters. Fixed grammar, unlimited passes. This is the case the pattern was designed for and where it is still unambiguously right.
Document and markup processing. A fixed node vocabulary with rendering, validation, indexing and transformation as separate operations.
Query and rule engines. A closed set of predicate types, with evaluation, explanation, optimisation and serialisation over them.
Serialisation across formats. One visitor per format over a fixed object model.
The pattern is common in these places and rare elsewhere, and that distribution is a fact worth trusting.
Why it is usually the wrong answer
The type set is rarely closed. Most domain models grow. Visitor optimises for the direction most business software does not move in, and once types start arriving, every addition edits every visitor.
Traversal and operation get entangled. Who walks the children — the node's accept, or the visitor's visit? Both appear in real code, and mixing them produces double visits and skipped subtrees. Decide once, write it down.
Encapsulation suffers. A visitor computing something meaningful usually needs the node's internals, so nodes end up exposing state they would otherwise hide.
Accumulating state is awkward. Visitors are often stateful — collecting results as they walk — which makes them non-reentrant and unsafe to reuse across trees.
It is hard to read. Following one operation means jumping between the visitor and every accept. New team members consistently find this the least approachable pattern in a codebase.
Check your understanding
Common mistakes
Applying it to an open type set. Every new type then edits every visitor.
A default visit method. New types are silently ignored by every operation.
Mixing traversal responsibilities. If both accept and visit recurse, subtrees are visited twice or skipped.
Stateful visitors reused across trees. Accumulated state from the previous walk leaks into the next.
Exposing node internals to satisfy visitors. The encapsulation the types had is traded away.
Using it where a dispatch table would do. For a dozen types, a map from kind to function is clearer and shorter.
Ignoring the language. Pattern matching over a closed union gives exhaustiveness with none of the ceremony.
Practice
A query builder has 8 predicate types and needs 5 operations over them: evaluate, render to SQL, explain in English, estimate cost, and validate. Design it, and say what would make you abandon Visitor later.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Check both axes: is the type set really closed, and are the five operations really the same shape?
Show a strong answer
This is close to Visitor's ideal case and I would use it, but only after checking both axes.
The type axis: eight predicate types — equality, comparison, range, set membership, text match, null check, and the AND/OR composites. That set is closed in the sense that matters: it is defined by what the query language supports, and extending it is a deliberate language decision made rarely, not a response to a new business requirement. That is the property Visitor needs.
The operation axis: five operations today and obviously more later — a formatter, an optimiser, an index-usage analyser, a serialiser. Operations are the growing axis. Correct fit.
The design:
- An explicit visitor interface with one method per predicate type, no default. Adding a ninth predicate must break the build in five places and list them. That compile error is the main safety property I am buying, and a default method would throw it away.
- Traversal owned by the visitor, not by
accept. Composites are where operations differ most:explainwants to render children with connecting words,estimateCostwants to combine child costs arithmetically, andvalidatemay want to stop at the first error. Ifacceptrecursed on the visitor's behalf, none of those could be expressed. I would write this decision down, because mixed traversal is the bug that produces double visits. - Visitors return values rather than accumulating into fields, so they are reentrant and safe to reuse.
explainreturns a string,estimateCostreturns a number. Where an operation genuinely needs accumulated state — collecting validation errors — it should take an explicit accumulator rather than hold one.
What would make me abandon it:
- Predicate types starting to arrive from product requirements rather than from language design. If someone adds a "within N kilometres" predicate this quarter and a "matches customer segment" one next, the type set is not closed and I am paying five edits per addition.
- Visitors needing predicate internals that the types would rather hide. If
renderToSqlhas to reach into a text-match predicate's tokenisation, the encapsulation is being traded away and the operation may belong on the type. - Operations diverging in shape. All five here are folds over a tree returning one value. An operation that had to rewrite the tree, or that only cared about two of the eight types, would fit badly — and a rewriting optimiser is exactly the kind of thing that arrives later.
The alternative I would keep in mind: if the language supported pattern matching over a closed union, I would use that instead and get exhaustiveness without any accept methods. Visitor here is a workaround for a language limitation, and it is worth being honest that this is what it is.
Explain why an operation implemented as a Visitor is harder for a new team member to follow than the same operation as a method on each type, and say what you would do to mitigate it.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Trace what a reader must do to answer 'what happens when we evaluate an addition?'
Show a strong answer
Trace the reading path, because that is where the difficulty is.
With methods on each type, the question "what happens when we evaluate an addition?" is answered by opening Addition and reading evaluate. One jump, and the surrounding context — the class's fields, its other operations — is right there.
With Visitor, the reader must: find the call site, see node.accept(visitor), open the node class to learn that accept calls visitAddition, open the visitor to find visitAddition, and then — because the visitor operates on a node whose internals it does not own — jump back to the node to understand what a.left and a.right are. Three or four jumps for one answer, and the accept step teaches them nothing: it is ceremony, and a reader cannot know that until they have read it and found nothing there.
The deeper problem is that the indirection carries no information. Most indirection in a codebase means something — an interface marks a substitution point, a factory marks a decision. accept marks the absence of a language feature. Readers reasonably assume it must be significant and spend time confirming that it is not.
There is also a second, worse reading task: "what does this node type participate in?" With methods, the answer is the class. With Visitor, it is distributed across every visitor, and finding them all is a search rather than a jump.
What I would do about it:
- Write down why Visitor was chosen, in a comment at the top of the visitor interface — the type set is closed, operations grow, here is the list of visitors. This is the single highest-value mitigation, because it converts "why is this so indirect?" into an answered question.
- Keep an index of visitors next to the node definitions, so the second reading task has an answer.
- Name visitors after what they produce, not after the pattern:
SqlRenderer,CostEstimator, notSqlVisitor. The reader learns what the class does from its name rather than from its mechanism. - Never mix traversal styles, and say which one is used. A reader who cannot tell whether children are walked by
acceptor byvisitcannot predict anything. - Provide one worked example in the codebase's own documentation, tracing a single node through a single visitor. Half an hour to write, and it removes the main onboarding cost permanently.
What I would not do: avoid the pattern purely because it is unfamiliar. Where the expression problem genuinely bites, the alternative — editing eight classes for every new operation — is worse for everyone including the new team member. The right response to a justified but hard-to-read structure is to explain it, not to replace it with something that fits the change pattern badly.
Key takeaways
- Visitor adds operations without editing types, inverting polymorphism's trade — this is the expression problem, and it has no free answer.
- Use it when the type set is closed and operations keep arriving: compilers, document models, query engines.
- Double dispatch is the mechanism; the
acceptmethods are ceremony compensating for a missing language feature. - Never use a default visit method. Explicit per-type methods turn a new type into a compile error listing every visitor to update.
- Decide once whether traversal lives in
acceptor invisit, or subtrees get visited twice or skipped. - Prefer returning values to accumulating state, so visitors are reentrant.
- Try a dispatch table or pattern matching over a closed union first — the latter gives exhaustiveness with none of the boilerplate.
- It is hard to read; if you use it, document why and name visitors after what they produce.
Further reading
- Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
- Philip Wadler, "The Expression Problem" (Java Genericity mailing list, 1998).
- Alfred Aho, Monica Lam, Ravi Sethi and Jeffrey Ullman, Compilers: Principles, Techniques, and Tools, second edition (Pearson, 2006).
- Robert Nystrom, Crafting Interpreters (Genever Benning, 2021) — Visitor applied to a syntax tree, with its costs discussed.
- Martin Odersky and Matthias Zenger, "Independently Extensible Solutions to the Expression Problem" (FOOL, 2005).