SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignVisitor Pattern
Software DesignBehavioral Patterns

Visitor Pattern

Separate algorithms from the objects on which they operate — add new operations without modifying the object structure.

February 8, 20256 min read
design-patternsjavavisitorbehavioraldouble-dispatch

One-line Definition

Separates an algorithm from the object structure it operates on, allowing you to add new behaviors to existing classes without altering them.


Problem

Suppose you have a complex object structure representing a company: Department, Manager, and Employee. You are asked to implement an "Export to XML" feature. You add an exportXML() method to all three classes. Next month, you are asked to add "Export to JSON", then "Calculate Annual Bonus", then "Generate HR Report". Every new feature forces you to modify every class in the hierarchy. This violates the Open/Closed Principle and clutters your domain models with unrelated business logic. The Visitor Pattern solves this by moving the operation out of the classes and into a standalone "Visitor" object.


Real-world Analogy

Think of a supermarket checkout. The scanner (Visitor) doesn't care if it's scanning a Bottle of Milk, a Loaf of Bread, or an Apple (Elements). The checkout system asks each item to "accept" the scanner. The item allows the scanner to read its barcode, and the scanner decides how to calculate the price. The items themselves don't know how to print receipts or calculate taxes; the scanner handles it.


Structure

  • Client
    • Creates the Visitor and passes it to the elements (usually navigating an Object Structure like a List or a Composite Tree).
  • Visitor
    • Interface declaring a visit(ConcreteElement) method for every type of element in the structure.
  • ConcreteVisitor
    • Implements the Visitor interface to execute a specific algorithm (e.g., XMLExportVisitor, TaxCalculationVisitor).
  • Element
    • Interface declaring an accept(Visitor) method.
  • ConcreteElement
    • Implements the accept() method, which simply calls the appropriate visit() method back on the Visitor (Double Dispatch).

Diagram

classDiagram
    class Client {
    }
    class Visitor {
        <<interface>>
        +visitElementA(ElementA)
        +visitElementB(ElementB)
    }
    class ConcreteVisitor {
        +visitElementA(ElementA)
        +visitElementB(ElementB)
    }
    class Element {
        <<interface>>
        +accept(Visitor)
    }
    class ConcreteElementA {
        +accept(Visitor)
    }
    class ConcreteElementB {
        +accept(Visitor)
    }
    
    Client --> Visitor
    Client --> Element
    ConcreteVisitor ..|> Visitor
    ConcreteElementA ..|> Element
    ConcreteElementB ..|> Element
    ConcreteElementA --> Visitor : calls visitElementA()

Code Walkthrough

Notice how the domain classes (Laptop, Mobile) only have one method related to the visitor: accept(). All the actual pricing logic is neatly extracted into the PostageVisitor. This is called Double Dispatch.

// 1. The Element Interface
interface ItemElement {
    void accept(ShoppingCartVisitor visitor);
}

// 2. Concrete Elements (Domain Models)
class Laptop implements ItemElement {
    private final int price;
    private final int weightGrams;

    public Laptop(int price, int weightGrams) {
        this.price = price;
        this.weightGrams = weightGrams;
    }

    public int getPrice() { return price; }
    public int getWeightGrams() { return weightGrams; }

    @Override
    public void accept(ShoppingCartVisitor visitor) {
        // Double Dispatch: The element passes ITSELF back to the visitor
        visitor.visit(this); 
    }
}

class Mobile implements ItemElement {
    private final int price;
    private final String brand;

    public Mobile(int price, String brand) {
        this.price = price;
        this.brand = brand;
    }

    public int getPrice() { return price; }
    public String getBrand() { return brand; }

    @Override
    public void accept(ShoppingCartVisitor visitor) {
        visitor.visit(this); 
    }
}

// 3. The Visitor Interface
interface ShoppingCartVisitor {
    void visit(Laptop laptop);
    void visit(Mobile mobile);
}

// 4. Concrete Visitor (The Algorithm)
class PostageVisitor implements ShoppingCartVisitor {
    private double totalPostage = 0;

    @Override
    public void visit(Laptop laptop) {
        // Laptops cost ₹50 per KG to ship
        double postage = (laptop.getWeightGrams() / 1000.0) * 50;
        System.out.println("Laptop postage: ₹" + postage);
        totalPostage += postage;
    }

    @Override
    public void visit(Mobile mobile) {
        // Mobiles have a flat ₹100 shipping fee, except Apple which is free
        double postage = mobile.getBrand().equalsIgnoreCase("Apple") ? 0 : 100;
        System.out.println(mobile.getBrand() + " Mobile postage: ₹" + postage);
        totalPostage += postage;
    }

    public double getTotalPostage() { return totalPostage; }
}

class Main {
    public static void main(String[] args) {
        List<ItemElement> cart = Arrays.asList(
            new Laptop(50000, 2500),
            new Mobile(80000, "Apple"),
            new Mobile(15000, "Samsung")
        );

        PostageVisitor calculator = new PostageVisitor();
        
        for (ItemElement item : cart) {
            item.accept(calculator); // Client triggers the visitor
        }

        System.out.println("Total Postage Cost: ₹" + calculator.getTotalPostage());
    }
}

Bad vs Good

Bad Approach

Problems

  • The domain models are cluttered with unrelated business logic (exportXML, calculatePostage).
  • Adding a new operation requires modifying every single class in the hierarchy.
interface Item {
    void exportXML();
    void calculatePostage();
}

class Laptop implements Item {
    public void exportXML() { /* XML logic */ }
    public void calculatePostage() { /* Postage logic */ }
}
// Imagine adding 10 more operations...

Better Approach

Improvements

  • Domain models are completely isolated and clean. They just accept a generic visitor.
  • Adding a new operation (e.g., TaxVisitor) requires zero modifications to the existing classes.
// Domain model stays clean forever
public void accept(Visitor v) { v.visit(this); } 

// New features are simply new Visitor classes
Visitor taxCalc = new TaxVisitor();
item.accept(taxCalc);

Pros vs Cons

ProsCons
Adding a new operation to the entire class hierarchy is trivialAdding a new Element to the hierarchy is extremely painful
Keeps domain classes hyper-focused and clean (Single Responsibility)The Visitor interface must know about every single Element class
Accumulates state across a complex object structure easilyViolates Encapsulation (Visitor might need public access to Element fields)
Uses Double-Dispatch to solve Java's lack of runtime type resolution

When to Use

  • You need to perform various, unrelated operations across a complex object structure (like a Composite tree).
  • The class hierarchy of your elements is highly stable and rarely changes, but you frequently need to add new operations/algorithms.
  • You want to clean up your domain models by extracting formatting, exporting, or calculating logic into separate classes.

When Not to Use

  • The Element class hierarchy changes frequently. (Adding a Tablet class requires updating the Visitor interface and every ConcreteVisitor!).
  • You can't expose the internal state of your elements through public getters, which the Visitor requires to do its job.

Real-world Examples

  • java.nio.file.FileVisitor (Used by Files.walkFileTree to execute logic on every file/directory visited).
  • javax.lang.model.element.ElementVisitor (Java Annotation Processing API).
  • Abstract Syntax Tree (AST) processing in compilers.

Key Takeaway

The Visitor Pattern uses a technique called Double Dispatch to cleanly separate algorithms from the data structures they operate on. Use it when your object structure (the Elements) is practically frozen, but you expect to add many new operations (the Visitors) over time. If your object structure is constantly adding new classes, avoid Visitor entirely, as the maintenance burden will be catastrophic.

PreviousObserver PatternNextProxy Pattern