Separating an algorithm from the object structure it operates on — a shape area/perimeter calculator that never modifies the shape classes themselves. Lower-frequency in interviews; recognize it, don't over-invest.
Published September 23, 2026
A quick honesty note before diving in: Visitor is genuinely less common in interviews than the other patterns in this chapter — worth recognizing on sight, not worth memorizing to the same depth as Strategy or Observer.
Normally, adding a new operation over a class hierarchy (Circle, Square, Triangle) means adding a new method to every class in that hierarchy. Visitor inverts this: the operation lives in a separate Visitor class, and each shape class only needs a single accept(visitor) method that never changes again, no matter how many new operations get added later.
interface Shape {
void accept(ShapeVisitor visitor); // the only method each shape needs — forever
}
interface ShapeVisitor {
void visit(Circle circle);
void visit(Square square);
void visit(Triangle triangle);
}
class Circle implements Shape {
double radius;
Circle(double radius) { this.radius = radius; }
public void accept(ShapeVisitor visitor) { visitor.visit(this); } // double-dispatch: picks the right overload
}
class Square implements Shape {
double side;
Square(double side) { this.side = side; }
public void accept(ShapeVisitor visitor) { visitor.visit(this); }
}
class Triangle implements Shape {
double base, height;
Triangle(double base, double height) { this.base = base; this.height = height; }
public void accept(ShapeVisitor visitor) { visitor.visit(this); }
}
class AreaVisitor implements ShapeVisitor {
double totalArea = 0;
public void visit(Circle c) { totalArea += Math.PI * c.radius * c.radius; }
public void visit(Square s) { totalArea += s.side * s.side; }
public void visit(Triangle t) { totalArea += 0.5 * t.base * t.height; }
}
class PerimeterVisitor implements ShapeVisitor {
double totalPerimeter = 0;
public void visit(Circle c) { totalPerimeter += 2 * Math.PI * c.radius; }
public void visit(Square s) { totalPerimeter += 4 * s.side; }
public void visit(Triangle t) { totalPerimeter += t.base + t.height * 2; /* simplified */ }
}
List<Shape> shapes = List.of(new Circle(5), new Square(4), new Triangle(3, 6));
AreaVisitor areaCalc = new AreaVisitor();
for (Shape s : shapes) s.accept(areaCalc); // each shape dispatches to its own visit() overload
System.out.println(areaCalc.totalArea);
Adding a new operation (say, a SerializationVisitor that converts shapes to JSON) means writing one new ShapeVisitor implementation — zero changes to Circle, Square, or Triangle. This is the tradeoff Visitor makes explicit: it's easy to add new operations, but hard to add a new shape (every existing ShapeVisitor implementation needs a new visit(NewShape) method) — the exact opposite tradeoff of the plain polymorphism you'd normally reach for, where adding a new shape is easy but adding a new operation means touching every shape class.
If calling code just had a Shape shape reference and called visitor.visit(shape) directly, Java's method overload resolution would pick visit(Shape) — there is no such overload, or worse, it would resolve based on the compile-time type, not the actual runtime shape. Routing through each concrete class's own accept() method (which knows its own compile-time type is exactly Circle, or Square, etc., from inside that class) is what makes visitor.visit(this) resolve to the correct overload at runtime — this two-step dispatch (first on the shape's real type via accept, then on the correct visit overload) is literally why the pattern is called "double dispatch."
Q: When is Visitor worth the extra structure over just adding methods to each shape class? A: When new operations are added far more often than new shape types — a stable, closed set of types (rarely changing) with a growing, open set of operations to perform on them is exactly the profile Visitor optimizes for. If new shape types are added frequently instead, Visitor actively hurts, since every existing visitor needs updating for each new type.
Q: Why is this called 'lower-frequency in interviews' compared to Strategy or Observer? A: Visitor solves a fairly specific structural tradeoff (operations vary more than types) that comes up less often in typical interview-style LLD prompts than 'swap an algorithm' (Strategy) or 'notify on state change' (Observer) — recognizing it when a prompt fits its shape is valuable, but it's reasonable to deprioritize memorizing it to the same depth.
Q: Could you implement this same idea using Java's pattern matching for switch (records + sealed interfaces) instead of the classic Visitor structure?
A: To a real degree, yes — modern Java's sealed interfaces plus exhaustive switch pattern matching can express "do different things per type, safely" without the accept()/visit() double-dispatch machinery, since the compiler itself can enforce exhaustiveness over a sealed hierarchy. Classic Visitor remains relevant for codebases on older Java versions, or when the operation set needs to stay open to types the current code doesn't know about (e.g. a plugin architecture), which a closed sealed hierarchy doesn't support by design.