Behavioural patterns organise how objects share work and communicate. In test frameworks you'll recognise them immediately: Strategy for choosing a browser or environment, Template Method in a BaseTest, Chain of Responsibility in request filters. This is Part 2; Part 1 covers Singleton, Decorator, Proxy and the structural patterns.

Strategy Pattern

Strategy = Swap algorithms at runtime

A navigation app can use different route strategies: fastest, shortest, avoid tolls. You switch strategy without changing the navigator. Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

// Strategy interface
public interface SortStrategy {
    void sort(int[] arr);
}

// Concrete strategies
public class BubbleSortStrategy implements SortStrategy {
    public void sort(int[] arr) { /* bubble sort */ }
}
public class QuickSortStrategy implements SortStrategy {
    public void sort(int[] arr) { /* quick sort */ }
}
public class MergeSortStrategy implements SortStrategy {
    public void sort(int[] arr) { /* merge sort */ }
}

// Context — uses a strategy
public class Sorter {
    private SortStrategy strategy;

    public Sorter(SortStrategy strategy) {
        this.strategy = strategy;
    }

    // Change strategy at runtime!
    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;
    }

    public void sort(int[] arr) {
        strategy.sort(arr); // delegates to strategy
    }
}

// Usage
Sorter sorter = new Sorter(new QuickSortStrategy());
sorter.sort(new int[]{5,1,3,2,4}); // uses QuickSort

sorter.setStrategy(new MergeSortStrategy());
sorter.sort(new int[]{5,1,3,2,4}); // now uses MergeSort

// Real-world: Collections.sort(list, comparator)
// comparator IS a Strategy for comparison!
Advertisement

Command Pattern

// Command: encapsulate a request as an object
// Supports: undo/redo, queuing, logging operations

public interface Command {
    void execute();
    void undo();
}

// Receiver
public class TextEditor {
    private StringBuilder text = new StringBuilder();

    public void write(String str) { text.append(str); }
    public void delete(int chars) {
        int start = Math.max(0, text.length() - chars);
        text.delete(start, text.length());
    }
    public String getText() { return text.toString(); }
}

// Concrete Commands
public class WriteCommand implements Command {
    private TextEditor editor;
    private String text;

    public WriteCommand(TextEditor editor, String text) {
        this.editor = editor; this.text = text;
    }
    public void execute() { editor.write(text); }
    public void undo() { editor.delete(text.length()); }
}

// Invoker — manages command history
public class CommandHistory {
    private Deque<Command> history = new ArrayDeque<>();

    public void execute(Command cmd) {
        cmd.execute();
        history.push(cmd);
    }

    public void undo() {
        if (!history.isEmpty()) history.pop().undo();
    }
}

// Usage
TextEditor editor = new TextEditor();
CommandHistory history = new CommandHistory();

history.execute(new WriteCommand(editor, "Hello "));
history.execute(new WriteCommand(editor, "World"));
System.out.println(editor.getText()); // Hello World

history.undo(); // undo "World"
System.out.println(editor.getText()); // Hello

Template Method Pattern

// Template Method: define skeleton of algorithm in base class,
// let subclasses override specific steps

public abstract class DataExporter {
    // Template method — final so subclasses can't change the order
    public final void export(String filename) {
        connectToSource();  // Step 1
        fetchData();        // Step 2
        processData();      // Step 3 (hook — optional override)
        writeToFile(filename); // Step 4
        cleanup();          // Step 5
    }

    protected abstract void connectToSource();
    protected abstract void fetchData();
    protected abstract void writeToFile(String filename);

    // Hook: subclass can override but doesn't have to
    protected void processData() {
        System.out.println("Default: no processing");
    }

    protected void cleanup() {
        System.out.println("Cleaning up connections");
    }
}

public class CSVExporter extends DataExporter {
    protected void connectToSource() { System.out.println("Connecting to DB"); }
    protected void fetchData() { System.out.println("Fetching rows"); }
    protected void writeToFile(String f) { System.out.println("Writing CSV: " + f); }

    @Override
    protected void processData() { System.out.println("Formatting as CSV"); }
}

public class ExcelExporter extends DataExporter {
    protected void connectToSource() { System.out.println("Connecting to API"); }
    protected void fetchData() { System.out.println("Fetching JSON"); }
    protected void writeToFile(String f) { System.out.println("Writing XLSX: " + f); }
}

new CSVExporter().export("report.csv");
new ExcelExporter().export("report.xlsx");

Chain of Responsibility

// Chain: pass request through a chain of handlers
// Each handler decides to process or pass to next

public abstract class SupportHandler {
    protected SupportHandler next;

    public SupportHandler setNext(SupportHandler next) {
        this.next = next;
        return next; // enables chaining
    }

    public abstract void handle(int level, String issue);
}

public class L1Support extends SupportHandler {
    public void handle(int level, String issue) {
        if (level <= 1) {
            System.out.println("L1 resolved: " + issue);
        } else if (next != null) {
            next.handle(level, issue);
        }
    }
}

public class L2Support extends SupportHandler {
    public void handle(int level, String issue) {
        if (level <= 2) {
            System.out.println("L2 resolved: " + issue);
        } else if (next != null) {
            next.handle(level, issue);
        }
    }
}

public class L3Support extends SupportHandler {
    public void handle(int level, String issue) {
        System.out.println("L3 (Engineering) resolved: " + issue);
    }
}

// Build chain
SupportHandler l1 = new L1Support();
SupportHandler l2 = new L2Support();
SupportHandler l3 = new L3Support();
l1.setNext(l2).setNext(l3);

l1.handle(1, "Password reset");  // L1 handles
l1.handle(2, "Account locked");  // L2 handles
l1.handle(3, "Data corruption"); // L3 handles
// Spring Security FilterChain is Chain of Responsibility!

Mediator Pattern

// Mediator: objects communicate through a mediator, not directly
// Reduces coupling — objects don't know about each other

public interface ChatMediator {
    void sendMessage(String message, User sender);
    void addUser(User user);
}

public class ChatRoom implements ChatMediator {
    private List<User> users = new ArrayList<>();

    public void addUser(User user) { users.add(user); }

    public void sendMessage(String message, User sender) {
        users.stream()
            .filter(u -> u != sender) // don't send to sender
            .forEach(u -> u.receive(message, sender.getName()));
    }
}

public abstract class User {
    protected ChatMediator mediator;
    protected String name;
    public User(ChatMediator m, String name) { this.mediator=m; this.name=name; }
    public String getName() { return name; }
    public abstract void send(String msg);
    public abstract void receive(String msg, String from);
}

public class ChatUser extends User {
    public ChatUser(ChatMediator m, String name) { super(m, name); }
    public void send(String msg) {
        System.out.println(name + " sends: " + msg);
        mediator.sendMessage(msg, this);
    }
    public void receive(String msg, String from) {
        System.out.println(name + " received from " + from + ": " + msg);
    }
}

ChatRoom room = new ChatRoom();
User alice = new ChatUser(room, "Alice");
User bob   = new ChatUser(room, "Bob");
User charlie = new ChatUser(room, "Charlie");
room.addUser(alice); room.addUser(bob); room.addUser(charlie);
alice.send("Hello everyone!");
// Bob received from Alice: Hello everyone!
// Charlie received from Alice: Hello everyone!

Memento Pattern

// Memento: capture and restore object state (undo feature)

// Memento — stores snapshot
public record EditorMemento(String content, int cursorPos) {}

// Originator — creates and restores mementos
public class TextEditor {
    private String content = "";
    private int cursorPos = 0;

    public void type(String text) {
        content = content.substring(0, cursorPos) + text
            + content.substring(cursorPos);
        cursorPos += text.length();
    }

    public EditorMemento save() {
        return new EditorMemento(content, cursorPos);
    }

    public void restore(EditorMemento m) {
        this.content = m.content();
        this.cursorPos = m.cursorPos();
    }

    public String getContent() { return content; }
}

// Caretaker — manages history
public class UndoManager {
    private Deque<EditorMemento> history = new ArrayDeque<>();
    private TextEditor editor;

    public UndoManager(TextEditor editor) { this.editor = editor; }

    public void save() { history.push(editor.save()); }

    public void undo() {
        if (!history.isEmpty()) editor.restore(history.pop());
    }
}

TextEditor editor = new TextEditor();
UndoManager undoMgr = new UndoManager(editor);

undoMgr.save(); editor.type("Hello");
undoMgr.save(); editor.type(" World");
System.out.println(editor.getContent()); // Hello World
undoMgr.undo();
System.out.println(editor.getContent()); // Hello
undoMgr.undo();
System.out.println(editor.getContent()); // (empty)

Visitor & Prototype Patterns

// VISITOR: add new operations to objects without modifying them
interface Shape { void accept(ShapeVisitor visitor); }

public class Circle implements Shape {
    double radius;
    Circle(double r) { this.radius = r; }
    public void accept(ShapeVisitor v) { v.visit(this); }
}
public class Rectangle implements Shape {
    double w, h;
    Rectangle(double w, double h) { this.w=w; this.h=h; }
    public void accept(ShapeVisitor v) { v.visit(this); }
}

interface ShapeVisitor {
    void visit(Circle c);
    void visit(Rectangle r);
}

// Add new operation without modifying shapes
class AreaCalculator implements ShapeVisitor {
    public void visit(Circle c) { System.out.println("Circle area: " + Math.PI*c.radius*c.radius); }
    public void visit(Rectangle r) { System.out.println("Rect area: " + r.w*r.h); }
}
class XMLExporter implements ShapeVisitor {
    public void visit(Circle c) { System.out.println("<circle radius='"+c.radius+"'/>"); }
    public void visit(Rectangle r) { System.out.println("<rect w='"+r.w+"' h='"+r.h+"'/>"); }
}

List<Shape> shapes = List.of(new Circle(5), new Rectangle(3,4));
ShapeVisitor area = new AreaCalculator();
shapes.forEach(s -> s.accept(area));
shapes.forEach(s -> s.accept(new XMLExporter()));

// PROTOTYPE: clone objects efficiently
public class Employee implements Cloneable {
    String name; String dept; List<String> skills;

    public Employee(String name, String dept, List<String> skills) {
        this.name=name; this.dept=dept; this.skills=new ArrayList<>(skills);
    }

    @Override
    public Employee clone() {
        try {
            Employee copy = (Employee) super.clone();
            copy.skills = new ArrayList<>(this.skills); // deep copy list
            return copy;
        } catch (CloneNotSupportedException e) { throw new RuntimeException(e); }
    }
}
Employee original = new Employee("Alice", "IT", List.of("Java","Spring"));
Employee clone = original.clone();
clone.name = "Bob"; // doesn't affect original

What is the difference between Decorator and Proxy?

Decorator: adds new behavior/responsibilities to an object (wraps to enhance). Proxy: controls access to an object (lazy loading, security check, caching). Both wrap the same interface, but Decorator changes what the object DOES, Proxy controls HOW it's accessed.

When would you use Visitor pattern?

When you have a stable class hierarchy but need to frequently ADD new operations. Without Visitor, adding a new operation means modifying every class. With Visitor, add a new Visitor class. Used in: compilers (AST traversal), document export (HTML/PDF/XML), tax calculators across different product types.

Design Patterns Interview Q&A

What is the difference between Strategy and State patterns?

Both use delegation and look similar. Strategy: algorithm is chosen by client and doesn't change (sorting algorithm). State: object changes its own state internally, and behavior changes with state (traffic light changes states automatically). In State, the state object may hold a reference to the context and change its state.

What is the difference between Decorator and Inheritance?

Inheritance is static (decided at compile time), can't combine multiple behaviors. Decorator is dynamic (compose at runtime), can stack multiple decorators. Decorator follows open/closed principle — add behavior without modifying existing classes. Java I/O uses Decorator: BufferedReader(FileReader(...)).

When would you use Observer pattern?

When multiple objects need to react to state changes in another object without tight coupling. Examples: event systems (DOM events, Spring events), MVC (model notifies views), stock price feeds (multiple traders notified on price change), Kafka consumers (multiple consumer groups react to one event).

What is the difference between Abstract Factory and Builder?

Abstract Factory: creates FAMILIES of related objects without specifying their concrete classes. Builder: constructs ONE complex object step by step, separating construction from representation. Abstract Factory returns the product immediately; Builder assembles it incrementally and returns via build().