The Pain of Scattered Business Logic
Picture this: You're building a clinic management system. You have three appointment types: general consultations, lab tests, and surgeries. Each is modelled as a clean class. Your first feature request is straightforward: calculate the billing fee for each appointment. You add a calculateFee() method to each class. Done.
Then the insurance team needs claim codes per appointment type. You add generateClaimCode() to each class. Then admin wants appointment summaries for records. You add generateSummary(). The compliance team needs discharge notes. Another method on every class.
Six months in, your clean appointment classes look like this:
public class Surgery {
private String patientName;
private String procedureName;
private int durationMinutes;
public double calculateFee() { /* billing logic */ }
public String generateClaimCode() { /* insurance code */ }
public String generateSummary() { /* appointment summary */ }
public String generateDischargeNote() { /* discharge logic */ }
// ...more unrelated methods keep piling up
}Every new operation means modifying every appointment class. Your classes have stopped being models of clinical appointments. They've become dumping grounds for every feature the business ever asked for. Adding a new operation is risky: you touch every class, risk introducing bugs, and trigger a full regression cycle.
The Visitor pattern solves this by separating operations from the object structure they work on. New operations become new visitor classes, and you never touch the appointment classes again.
Understanding the Visitor Pattern
The Visitor pattern lets you define a new operation without changing the classes of the elements it operates on. You create a visitor object that encapsulates the operation, and each element in the structure "accepts" the visitor and delegates the operation back to it.
Think of it like a clinic administrator processing different appointment types: the administrator (visitor) knows how to handle a general consultation, a lab test, and a surgery differently. Each appointment doesn't need to know how it gets processed; it just hands itself to the administrator. When the insurance team sends their own claims processor, the appointments don't change, only the processor does.
This pattern promotes Open/Closed Principle, Single Responsibility, and Separation of Concerns by keeping operations in dedicated visitor classes rather than scattered across the element hierarchy.

Visitor Pattern Components
Core Components
Element Interface: Declares an accept(visitor) method that each concrete element must implement
Visitor Interface: Declares a visit() method overload for each concrete element type
Concrete Elements: The object structure (GeneralConsultation, LabTest, Surgery). Each calls visitor.visit(this) in its accept method
Concrete Visitors: Implement the operation for every element type (BillingVisitor, InsuranceClaimVisitor)
Complete Java Implementation
Let's build a clinic management system that demonstrates the Visitor pattern's power in adding new operations without touching appointment classes.
Element Interface
public interface Appointment {
void accept(AppointmentVisitor visitor);
String getPatientName();
}Visitor Interface
public interface AppointmentVisitor {
void visit(GeneralConsultation consultation);
void visit(LabTest labTest);
void visit(Surgery surgery);
}Concrete Elements
public class GeneralConsultation implements Appointment {
private String patientName;
private String doctorName;
public GeneralConsultation(String patientName, String doctorName) {
this.patientName = patientName;
this.doctorName = doctorName;
}
public String getDoctorName() { return doctorName; }
@Override public String getPatientName() { return patientName; }
@Override public void accept(AppointmentVisitor visitor) { visitor.visit(this); }
}
public class LabTest implements Appointment {
private String patientName;
private String testType;
public LabTest(String patientName, String testType) {
this.patientName = patientName;
this.testType = testType;
}
public String getTestType() { return testType; }
@Override public String getPatientName() { return patientName; }
@Override public void accept(AppointmentVisitor visitor) { visitor.visit(this); }
}
public class Surgery implements Appointment {
private String patientName;
private String procedureName;
public Surgery(String patientName, String procedureName) {
this.patientName = patientName;
this.procedureName = procedureName;
}
public String getProcedureName() { return procedureName; }
@Override public String getPatientName() { return patientName; }
@Override public void accept(AppointmentVisitor visitor) { visitor.visit(this); }
}Concrete Visitors
public class BillingVisitor implements AppointmentVisitor {
private double totalBill;
@Override
public void visit(GeneralConsultation consultation) {
double fee = 80.00;
totalBill += fee;
System.out.println(consultation.getPatientName() + " [Consultation] $" + fee + " (Dr. " + consultation.getDoctorName() + ")");
}
@Override
public void visit(LabTest labTest) {
double fee = 45.00;
totalBill += fee;
System.out.println(labTest.getPatientName() + " [Lab Test] $" + fee + " (" + labTest.getTestType() + ")");
}
@Override
public void visit(Surgery surgery) {
double fee = 500.00;
totalBill += fee;
System.out.println(surgery.getPatientName() + " [Surgery] $" + fee + " (" + surgery.getProcedureName() + ")");
}
public double getTotalBill() { return totalBill; }
}public class InsuranceClaimVisitor implements AppointmentVisitor {
@Override
public void visit(GeneralConsultation consultation) {
System.out.println(consultation.getPatientName() + " Code: GP-001 Type: General Practice");
}
@Override
public void visit(LabTest labTest) {
System.out.println(labTest.getPatientName() + " Code: LT-042 Type: Diagnostic Lab");
}
@Override
public void visit(Surgery surgery) {
System.out.println(surgery.getPatientName() + " Code: SG-301 Type: Surgical Procedure");
}
}Client
public class ClinicBillingDemo {
public static void main(String[] args) {
System.out.println("=== Clinic Management System ===\n");
List<Appointment> appointments = Arrays.asList(
new GeneralConsultation("Alice Johnson", "Smith"),
new LabTest("Bob Williams", "Blood Panel"),
new Surgery("Carol Davis", "Appendectomy"),
new GeneralConsultation("David Brown", "Patel")
);
System.out.println("--- Insurance Claims ---");
InsuranceClaimVisitor claims = new InsuranceClaimVisitor();
for (Appointment appointment : appointments) {
appointment.accept(claims);
}
System.out.println("\n--- Patient Bills ---");
BillingVisitor billing = new BillingVisitor();
for (Appointment appointment : appointments) {
appointment.accept(billing);
}
System.out.println("Total: $" + billing.getTotalBill());
}
}Expected Output:
=== Clinic Management System ===
--- Insurance Claims ---
Alice Johnson Code: GP-001 Type: General Practice
Bob Williams Code: LT-042 Type: Diagnostic Lab
Carol Davis Code: SG-301 Type: Surgical Procedure
David Brown Code: GP-001 Type: General Practice
--- Patient Bills ---
Alice Johnson [Consultation] $80.0 (Dr. Smith)
Bob Williams [Lab Test] $45.0 (Blood Panel)
Carol Davis [Surgery] $500.0 (Appendectomy)
David Brown [Consultation] $80.0 (Dr. Patel)
Total: $705.0🚀 Get the Complete Implementation
The full code with additional visitors (risk analysis, compliance audit) is available in our Design Patterns Repository.
# Clone and run the complete demo
git clone https://github.com/shift-elevate/design-patterns.git
cd design-patterns
mvn test -Dtest=VisitorPatternTestReal World Examples
The Visitor pattern is extensively used in real-world systems where stable structures need evolving operations:
1. Compiler Abstract Syntax Trees
Compilers build an AST (Abstract Syntax Tree) representing the parsed source code. Different compiler phases (type checking, optimisation, code generation, linting) are implemented as separate visitors that traverse the same tree. The AST node classes remain untouched while each new compiler phase is a new visitor, making compiler toolchains highly extensible.
2. Document Export
Applications that model documents as a tree of elements (paragraphs, headings, tables, images) use the Visitor pattern for export operations. Each export format (PDF, HTML, plain text) is implemented as a separate visitor that knows how to render every element type, without any element class needing to know about export formats.
3. Static Code Analysis
Static analysis tools analyse source code by visiting AST nodes. Each analysis rule (detect null dereference, find unused variables, enforce naming conventions) is an independent visitor. Adding a new rule means adding a new visitor, and the AST structure and existing rules are never modified.
When to Use the Visitor Pattern
Understanding when to apply the Visitor pattern is crucial for making the right architectural decisions. Here's when it shines and when alternatives might be better:
✅ Ideal Scenarios:
You have a stable object structure but need to frequently add new operations.
You want to keep unrelated operations out of the element classes.
A class hierarchy contains many distinct types and you need type-specific behaviour across multiple operations.
You need to accumulate state across elements during traversal (like totalling tax across all assets).
❌ Skip It When:
The element hierarchy changes frequently, as adding a new element type requires updating every visitor.
The object structure is simple and has only a handful of operations that rarely change.
Encapsulation of element internals is critical, as visitors often need access to internal fields.
The double-dispatch mechanism adds more complexity than the problem warrants.
Next Steps: Apply the Visitor Pattern in Your Project
Ready to implement the Visitor pattern in your own projects? Here's a structured approach to get you started:
Identify Stable Structures: Find class hierarchies that rarely gain new types but frequently gain new operations.
Define the Visitor Interface: Declare one method per concrete element type so each visitor knows how to handle every element.
Add Accept to Elements: Each element implements a method that hands itself to the visitor, keeping all operation logic out of the element.
Create Concrete Visitors: Implement each operation as a dedicated visitor class with its own state.
Drive from the Client: Iterate the element collection and let each element accept the visitor in turn.
The Visitor pattern transforms bloated element classes into clean, focused models by relocating operations to dedicated visitor classes that can evolve independently. Your element classes model your domain; your visitor classes model what you do with it.
Found this helpful? Share it with a colleague whose domain classes are drowning in unrelated operations. Got questions? We'd love to hear from you at [email protected]

