Visitor Pattern
The Visitor pattern lets you add new operations to a fixed family of element types without touching those types, and it works through double dispatch: the call element.accept(visitor) dispatches once on the element's runtime type to land in Food.accept, which then calls visitor.visitFood(this) — a second dispatch on the visitor's runtime type. Two virtual calls in a row resolve both the element type and the operation type, which is the one thing a single method call in most OO languages cannot do on its own.
Why the naive single-dispatch version fails
The instinct is to skip the dance and just write one method that switches on type:
// BROKEN: tries to pick the operation from one dispatch
void applyHoliday(Product p) {
if (p instanceof Food) { /* food logic */ }
else if (p instanceof Clothing) { /* clothing logic */ }
else if (p instanceof Electronics) { /* electronics logic */ }
// forgot a type? silent fall-through, no compile error
}This compiles, but it pushes the type decision into a runtime instanceof ladder. Add a fourth product and every such ladder, scattered across every operation, must be found and edited — the compiler will not remind you. Double dispatch replaces the ladder with the language's own method resolution: accept already knows it is a Food (no instanceof), and the visitor interface forces every visitor to implement visitFood (a missing one is a compile error). The naive version trades that compile-time safety for a runtime guess.
Traced example with real values
Three products in a cart, two discount visitors. Each product carries a base price; each visitor applies a percentage based on the concrete product type. Trace the call food.accept(holidayVisitor) and its siblings:
| Call | Dispatch 1 (element) | Dispatch 2 (visitor) | Rule fired | Result |
|---|---|---|---|---|
food.accept(holiday) | → Food.accept | → Holiday.visitFood | food: 5% off | $20.00 → $19.00 |
clothing.accept(clearance) | → Clothing.accept | → Clearance.visitClothing | clothing: 40% off | $80.00 → $48.00 |
electronics.accept(holiday) | → Electronics.accept | → Holiday.visitElectronics | electronics: 10% off | $500.00 → $450.00 |
Notice no instanceof and no type tag anywhere: the pair (product type, discount strategy) is selected purely by which method the two virtual calls resolve to. Swap holiday for clearance on the same product and a totally different cell of the matrix fires — that is double dispatch picking one cell from the (elements × visitors) grid.
Working code (Java)
The accept/visit dance, completed so the trace above actually runs and prints the numbers:
interface DiscountVisitor {
double visitFood(Food f);
double visitClothing(Clothing c);
double visitElectronics(Electronics e);
}
interface Product { // Element interface
double accept(DiscountVisitor v);
double price();
}
class Food implements Product {
private final double price;
Food(double p) { this.price = p; }
public double price() { return price; }
public double accept(DiscountVisitor v) { return v.visitFood(this); } // dispatch 1
}
class Clothing implements Product {
private final double price;
Clothing(double p) { this.price = p; }
public double price() { return price; }
public double accept(DiscountVisitor v) { return v.visitClothing(this); }
}
class Electronics implements Product {
private final double price;
Electronics(double p) { this.price = p; }
public double price() { return price; }
public double accept(DiscountVisitor v) { return v.visitElectronics(this); }
}
class HolidayDiscountVisitor implements DiscountVisitor { // dispatch 2 lands here
public double visitFood(Food f) { return f.price() * 0.95; } // 5% off
public double visitClothing(Clothing c) { return c.price() * 0.90; } // 10% off
public double visitElectronics(Electronics e){ return e.price() * 0.90; } // 10% off
}
class ClearanceDiscountVisitor implements DiscountVisitor {
public double visitFood(Food f) { return f.price() * 0.80; }
public double visitClothing(Clothing c) { return c.price() * 0.60; } // 40% off
public double visitElectronics(Electronics e){ return e.price() * 0.85; }
}
public class Solution {
public static void main(String[] args) {
Product food = new Food(20.0);
Product clothing = new Clothing(80.0);
Product electronics = new Electronics(500.0);
var holiday = new HolidayDiscountVisitor();
var clearance = new ClearanceDiscountVisitor();
System.out.printf("%.2f%n", food.accept(holiday)); // 19.00
System.out.printf("%.2f%n", clothing.accept(clearance)); // 48.00
System.out.printf("%.2f%n", electronics.accept(holiday)); // 450.00
}
}The Go equivalent uses a method on each concrete type calling back into the visitor interface — the same two dispatches, since Go has no overloading: name the methods VisitFood, VisitClothing, VisitElectronics on the visitor interface and have each type's Accept(v Visitor) call the right one.
Pitfalls
- Adding an element breaks every visitor. Add
Toysand you must addvisitToysto theDiscountVisitorinterface, which instantly breaks compilation of every existing visitor until each implements it. Visitor optimizes for adding operations, and pays for it on the element axis. Only reach for it when the element set is stable. - Tempting to delete
acceptand doinstanceofin the visitor. This destroys the whole point: you lose the second dispatch and the compile-time exhaustiveness, and the visitor must now know every concrete type by hand. - Cyclic dependency. The visitor interface names every concrete element type, and every element names the visitor — the two packages are mutually dependent and cannot be split cleanly. This is inherent, not a mistake.
- Broken encapsulation. To do useful work, visitors often need access to element internals, pushing you toward public getters or package-private exposure that weakens the element's encapsulation.
- Accumulating state in a stateful visitor (e.g. a running total) makes the visitor non-reentrant: reusing one instance across two traversals corrupts the result. Either create a fresh visitor per traversal or pass an explicit accumulator.
When to use it / when NOT to
Signals that point here: a fixed set of element types (an AST, a DOM, a shape hierarchy, a fixed product catalog); many distinct, unrelated operations over them (type-check, pretty-print, optimize, serialize); and you want each operation's logic in one cohesive place rather than smeared as a method on every element class.
Trade-offs vs alternatives
- vs. polymorphic methods on the elements (the default OO approach: put
applyDiscount()on each product). You gain easy addition of elements — a new product just implements the interface — but adding a new operation means editing every element class, and unrelated operations pile up inside each element, breaking single responsibility. Visitor inverts exactly this: cheap new operations, expensive new elements. - vs. an
instanceof/ type-switch ladder (or Go type switch). The ladder is fewer classes and reads simply for two or three types, but it gives no compile-time exhaustiveness (forget a case and it silently falls through), and the same ladder gets copy-pasted into every operation. Visitor centralizes and makes missing cases a compile error, at the cost of the accept/visit boilerplate and extra classes. - vs. Strategy. Strategy varies one algorithm behind a single interface call (single dispatch on the strategy). Visitor varies an algorithm across a type hierarchy — it is Strategy plus a second dispatch on element type. If your behavior does not branch on the concrete element type, you do not need Visitor; use Strategy.
Decision rule: choose Visitor when element types are stable but operations grow and each must branch per element type; prefer methods on the elements when the operation set is stable but you keep adding element types; prefer Strategy when the behavior varies but not by element type.
Takeaways
- Double dispatch is the whole mechanism:
acceptresolves the element type, thevisit*callback resolves the operation type — two virtual calls select one cell of the (elements × visitors) matrix, with noinstanceof. - Visitor makes adding operations cheap and adding element types expensive — the exact opposite of putting methods on the elements. Pick the axis you expect to grow.
- Use it when the element set is fixed (ASTs, DOMs, fixed catalogs) and operations multiply; the payoff is compile-time exhaustiveness and one operation per cohesive class.
- The costs are real: cyclic element↔visitor coupling, weakened encapsulation, more classes, and a forced edit to every visitor on each new element.
Re-authored and deepened for this guide. Sources: Gamma, Helm, Johnson & Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (1994), the canonical Visitor / double-dispatch treatment; Robert Nystrom, Crafting Interpreters, ch. "Representing Code" (Visitor over an AST); the Refactoring.Guru entry on Visitor; and the original GeeksforGeeks retail-discount example that this page expands. Worked numbers, the double-dispatch diagram, the broken single-dispatch counter-example, and the selection/trade-off analysis were added for this guide.
The Expression Problem — and a modern alternative
Visitor's "cheap operations, expensive elements" trade-off is one face of the Expression Problem: no single technique lets you add both new element types and new operations without editing existing code. Putting methods on the elements makes new elements cheap and new operations expensive; Visitor makes exactly the opposite choice. If you genuinely need both axes open, Visitor is the wrong tool — reach for an operation registry keyed by type, or a language with multi-methods (Clojure, Julia) where dispatch is symmetric on all arguments.
In modern Java (17+), a sealed interface plus a switch with pattern matching is often the better replacement for Visitor when the element set is stable but you dislike the accept/visit boilerplate: sealed interface Product permits Food, Clothing, Electronics makes the compiler enforce that a switch (product) covers every permitted subtype — the same exhaustiveness Visitor buys, but without the double-dispatch ceremony and the cyclic element↔visitor coupling. The tradeoff flips back if elements change often: with a sealed switch you edit every switch when you add a type (like Visitor edits every visitor), whereas plain polymorphic methods on the elements absorb a new type for free. Choose the sealed-switch when operations grow and you want exhaustiveness without accept(); choose Visitor when you cannot use a modern language feature or must dispatch across two independent hierarchies.
A trap the compiler will NOT catch
If you write the visitor with method overloads (visit(Shape), visit(Circle)) instead of distinctly-named methods, Java resolves the overload on the static type at the accept call site — so visitor.visit(this) where this is statically Shape silently calls visit(Shape), never visit(Circle). Compilation stays green; behavior is wrong. This is why the canonical Visitor uses distinct names (visitFood, visitClothing) — it forces the second dispatch to land on the concrete type rather than collapsing to an overload chosen at compile time.
🤖 Don't fully get this? Learn it with Claude
Stuck on Visitor Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Visitor Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Visitor Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Visitor Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Visitor Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Visitor Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.