This fifth and final installment focuses on behavioral patterns: how objects collaborate, how they communicate, and how they share responsibilities. You’ll see when to choose each pattern to decouple logic, encapsulate decisions, propagate events, or model workflows, with modern implementations in Java 25.
Behavioral Design Patterns
Behavioral patterns focus on communication between objects, defining how they interact and distribute responsibilities.
Chain of Responsibility
The Chain of Responsibility pattern allows passing requests along a chain of potential handlers. Each handler decides whether to process the request or pass it to the next in the chain. This decouples the sender of the request from its receivers, giving multiple objects a chance to handle it.
This pattern provides flexibility to determine dynamically which object handles each request. It’s especially useful for implementing processing pipelines, middlewares, or validation systems where multiple filters must be applied in sequence.
Example: HTTP middleware pipeline
// Handler interfacepublic interface HttpHandler { HttpResponse handle(HttpRequest request);}
// Abstract base class for middlewarepublic abstract class Middleware implements HttpHandler { protected HttpHandler next;
public Middleware setNext(HttpHandler next) { this.next = next; return this; }
protected HttpResponse passToNext(HttpRequest request) { if (next != null) { return next.handle(request); } return HttpResponse.notFound("No handler found"); }}
// Logging middlewarepublic class LoggingMiddleware extends Middleware {
private static final Logger log = Logger.getLogger(LoggingMiddleware.class.getName());
@Override public HttpResponse handle(HttpRequest request) { long startTime = System.currentTimeMillis(); String requestId = UUID.randomUUID().toString().substring(0, 8);
log.info("[%s] → %s %s".formatted(requestId, request.method(), request.path()));
HttpResponse response = passToNext(request);
long duration = System.currentTimeMillis() - startTime; log.info("[%s] ← %d (%dms)".formatted(requestId, response.status(), duration));
return response; }}
// Cache middlewarepublic class CacheMiddleware extends Middleware {
private final Cache<String, HttpResponse> cache; private final Duration ttl;
public CacheMiddleware(Cache<String, HttpResponse> cache, Duration ttl) { this.cache = cache; this.ttl = ttl; }
@Override public HttpResponse handle(HttpRequest request) { // Only cache GET requests if (!"GET".equals(request.method())) { return passToNext(request); }
String cacheKey = request.method() + ":" + request.path() + ":" + request.queryString();
HttpResponse cached = cache.get(cacheKey); if (cached != null) { return cached.withHeader("X-Cache", "HIT"); }
HttpResponse response = passToNext(request);
if (response.status() == 200) { cache.put(cacheKey, response, ttl); }
return response.withHeader("X-Cache", "MISS"); }}
// Final controller (ends the chain)public class ApiController implements HttpHandler {
private final Map<String, Function<HttpRequest, HttpResponse>> routes;
public ApiController() { this.routes = new HashMap<>(); }
public void addRoute(String path, Function<HttpRequest, HttpResponse> handler) { routes.put(path, handler); }
@Override public HttpResponse handle(HttpRequest request) { return routes.getOrDefault(request.path(), r -> HttpResponse.notFound("Route not found")) .apply(request); }}
// Builder to construct the pipelinepublic class MiddlewarePipeline {
private final List<Middleware> middlewares = new ArrayList<>(); private HttpHandler finalHandler;
public MiddlewarePipeline use(Middleware middleware) { middlewares.add(middleware); return this; }
public MiddlewarePipeline endpoint(HttpHandler handler) { this.finalHandler = handler; return this; }
public HttpHandler build() { if (finalHandler == null) { throw new IllegalStateException("Final handler not set"); }
HttpHandler current = finalHandler; for (int i = middlewares.size() - 1; i >= 0; i--) { middlewares.get(i).setNext(current); current = middlewares.get(i); }
return current; }}
// Using Chain of Responsibilityfinal var controller = new ApiController();controller.addRoute("/api/users", req -> HttpResponse.ok("{\"users\": []}"));controller.addRoute("/api/products", req -> HttpResponse.ok("{\"products\": []}"));
final var pipeline = new MiddlewarePipeline() .use(new LoggingMiddleware()) .use(new CacheMiddleware(new LRUCache<>(), Duration.ofMinutes(5))) .endpoint(controller) .build();
HttpResponse response = pipeline.handle(new HttpRequest("GET", "/api/users"));Command
The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support reversible operations. It decouples the object that invokes the operation from the one that knows how to execute it.
Each command is a self-contained object that contains all the information needed to execute an action. This allows creating command queues, implementing undo/redo, logging operation history, and creating composite commands (macros).
Example: Text editor with undo/redo
// Command interfacepublic interface TextCommand { void execute(); void undo(); String description();}
// Receiver - the document on which commands operatepublic class TextDocument { private final StringBuilder content; private final List<TextDocumentListener> listeners = new ArrayList<>();
public TextDocument() { this.content = new StringBuilder(); }
public TextDocument(String initialContent) { this.content = new StringBuilder(initialContent); }
public void insert(int position, String text) { content.insert(position, text); notifyListeners(); }
public String delete(int start, int length) { String deleted = content.substring(start, start + length); content.delete(start, start + length); notifyListeners(); return deleted; }
public String replace(int start, int end, String newText) { String replaced = content.substring(start, end); content.replace(start, end, newText); notifyListeners(); return replaced; }
public String getText() { return content.toString(); }
public int length() { return content.length(); }
public void addListener(TextDocumentListener listener) { listeners.add(listener); }
private void notifyListeners() { listeners.forEach(l -> l.onDocumentChanged(this)); }}
// Concrete commandspublic final class InsertCommand implements TextCommand { private final TextDocument document; private final int position; private final String text;
public InsertCommand(TextDocument document, int position, String text) { this.document = document; this.position = position; this.text = text; }
@Override public void execute() { document.insert(position, text); }
@Override public void undo() { document.delete(position, text.length()); }
@Override public String description() { return "Insert '%s' at position %d".formatted( text.length() > 20 ? text.substring(0, 20) + "..." : text, position); }}
public final class DeleteCommand implements TextCommand { private final TextDocument document; private final int start; private final int length; private String deletedText;
public DeleteCommand(TextDocument document, int start, int length) { this.document = document; this.start = start; this.length = length; }
@Override public void execute() { deletedText = document.delete(start, length); }
@Override public void undo() { document.insert(start, deletedText); }
@Override public String description() { return "Delete %d characters at position %d".formatted(length, start); }}
// Other commands ...
// Composite command (Macro)public final class MacroCommand implements TextCommand { private final String name; private final List<TextCommand> commands;
public MacroCommand(String name, List<TextCommand> commands) { this.name = name; this.commands = new ArrayList<>(commands); }
@Override public void execute() { commands.forEach(TextCommand::execute); }
@Override public void undo() { // Undo in reverse order for (int i = commands.size() - 1; i >= 0; i--) { commands.get(i).undo(); } }
@Override public String description() { return "Macro '%s' (%d commands)".formatted(name, commands.size()); }}
// Invoker - manages history and undo/redopublic class CommandHistory { private final Deque<TextCommand> undoStack = new ArrayDeque<>(); private final Deque<TextCommand> redoStack = new ArrayDeque<>(); private final int maxHistory;
public CommandHistory(int maxHistory) { this.maxHistory = maxHistory; }
public void execute(TextCommand command) { command.execute(); undoStack.push(command); redoStack.clear(); // Clear redo when executing new command
// Keep history limit while (undoStack.size() > maxHistory) { undoStack.removeLast(); } }
public boolean canUndo() { return !undoStack.isEmpty(); }
public boolean canRedo() { return !redoStack.isEmpty(); }
public void undo() { if (canUndo()) { TextCommand command = undoStack.pop(); command.undo(); redoStack.push(command); } }
public void redo() { if (canRedo()) { TextCommand command = redoStack.pop(); command.execute(); undoStack.push(command); } }
public List<String> getUndoHistory() { return undoStack.stream().map(TextCommand::description).toList(); }
public List<String> getRedoHistory() { return redoStack.stream().map(TextCommand::description).toList(); }}
// Client - Text editorpublic class TextEditor { private final TextDocument document; private final CommandHistory history; private int cursorPosition = 0;
public TextEditor() { this.document = new TextDocument(); this.history = new CommandHistory(100); }
public void type(String text) { history.execute(new InsertCommand(document, cursorPosition, text)); cursorPosition += text.length(); }
public void delete(int length) { if (cursorPosition > 0 && length > 0) { int deleteStart = Math.max(0, cursorPosition - length); int actualLength = cursorPosition - deleteStart; history.execute(new DeleteCommand(document, deleteStart, actualLength)); cursorPosition = deleteStart; } }
public void replaceSelection(int selectionLength, String newText) { history.execute(new ReplaceCommand( document, cursorPosition, cursorPosition + selectionLength, newText)); cursorPosition += newText.length(); }
public void undo() { history.undo(); } public void redo() { history.redo(); }
public String getText() { return document.getText(); }}
// Using the Command patternfinal var editor = new TextEditor();
editor.type("Hello ");editor.type("World");IO.println(editor.getText()); // "Hello World"
editor.undo();IO.println(editor.getText()); // "Hello "
editor.redo();IO.println(editor.getText()); // "Hello World"
editor.type("!");IO.println(editor.getText()); // "Hello World!"Interpreter
The Interpreter pattern defines a grammatical representation for a language and an interpreter that uses that representation to interpret sentences in the language. It’s useful when a problem occurs frequently and can be expressed in a simple language.
The pattern uses an abstract syntax tree (AST) where each node is an expression. Terminal expressions represent atomic elements of the language, while non-terminal ones compose other expressions. Although not the most efficient, it’s ideal for simple domain-specific languages (DSL).
Example: Filter expression interpreter
// Interpretation contextpublic record FilterContext<T>(T item, Map<String, Object> variables) {
@SuppressWarnings("unchecked") public <V> V getVariable(String name) { return (V) variables.get(name); }
public Object getProperty(String propertyName) { try { var getter = item.getClass().getMethod(propertyName); return getter.invoke(item); } catch (Exception e) { throw new RuntimeException("Cannot access property: " + propertyName, e); } }}
// Abstract expressionpublic sealed interface FilterExpression<T> permits PropertyExpression, LiteralExpression, ComparisonExpression, AndExpression, OrExpression, NotExpression, InExpression {
boolean interpret(FilterContext<T> context); String toQueryString();}
// Terminal expressionspublic record PropertyExpression<T>(String propertyName) implements FilterExpression<T> { @Override public boolean interpret(FilterContext<T> context) { Object value = context.getProperty(propertyName); return value instanceof Boolean b ? b : value != null; }
@Override public String toQueryString() { return propertyName; }
public Object getValue(FilterContext<T> context) { return context.getProperty(propertyName); }}
// Non-terminal expressions - comparisonspublic record ComparisonExpression<T>( PropertyExpression<T> left, ComparisonOperator operator, FilterExpression<T> right) implements FilterExpression<T> {
public enum ComparisonOperator { EQUALS("="), NOT_EQUALS("!="), GREATER_THAN(">"), LESS_THAN("<"), GREATER_OR_EQUALS(">="), LESS_OR_EQUALS("<="), CONTAINS("CONTAINS"), STARTS_WITH("STARTS_WITH");
private final String symbol; ComparisonOperator(String symbol) { this.symbol = symbol; } public String symbol() { return symbol; } }
@Override public boolean interpret(FilterContext<T> context) { Object leftValue = left.getValue(context); Object rightValue = right instanceof LiteralExpression<T> lit ? lit.value() : right.interpret(context);
return switch (operator) { case EQUALS -> Objects.equals(leftValue, rightValue); case NOT_EQUALS -> !Objects.equals(leftValue, rightValue); case GREATER_THAN -> compare(leftValue, rightValue) > 0; case LESS_THAN -> compare(leftValue, rightValue) < 0; case GREATER_OR_EQUALS -> compare(leftValue, rightValue) >= 0; case LESS_OR_EQUALS -> compare(leftValue, rightValue) <= 0; case CONTAINS -> String.valueOf(leftValue).contains(String.valueOf(rightValue)); case STARTS_WITH -> String.valueOf(leftValue).startsWith(String.valueOf(rightValue)); }; }
@SuppressWarnings("unchecked") private int compare(Object a, Object b) { if (a instanceof Comparable c1 && b instanceof Comparable c2) { return c1.compareTo(c2); } throw new IllegalArgumentException("Cannot compare: " + a + " with " + b); }
@Override public String toQueryString() { return "%s %s %s".formatted(left.toQueryString(), operator.symbol(), right.toQueryString()); }}
// Logical expressionspublic record AndExpression<T>(FilterExpression<T> left, FilterExpression<T> right) implements FilterExpression<T> {
@Override public boolean interpret(FilterContext<T> context) { return left.interpret(context) && right.interpret(context); }
@Override public String toQueryString() { return "(%s AND %s)".formatted(left.toQueryString(), right.toQueryString()); }}
public record OrExpression<T>(FilterExpression<T> left, FilterExpression<T> right) implements FilterExpression<T> {
@Override public boolean interpret(FilterContext<T> context) { return left.interpret(context) || right.interpret(context); }
@Override public String toQueryString() { return "(%s OR %s)".formatted(left.toQueryString(), right.toQueryString()); }}
// Other implementations
// DSL builder for building expressionspublic class FilterBuilder<T> {
public PropertyBuilder<T> where(String property) { return new PropertyBuilder<>(new PropertyExpression<>(property)); }
public static class PropertyBuilder<T> { private final PropertyExpression<T> property;
PropertyBuilder(PropertyExpression<T> property) { this.property = property; }
public FilterExpression<T> equals(Object value) { return new ComparisonExpression<>(property, ComparisonExpression.ComparisonOperator.EQUALS, new LiteralExpression<>(value)); }
public FilterExpression<T> greaterThan(Object value) { return new ComparisonExpression<>(property, ComparisonExpression.ComparisonOperator.GREATER_THAN, new LiteralExpression<>(value)); }
public FilterExpression<T> contains(String value) { return new ComparisonExpression<>(property, ComparisonExpression.ComparisonOperator.CONTAINS, new LiteralExpression<>(value)); }
public FilterExpression<T> in(Object... values) { return new InExpression<>(property, List.of(values)); } }
public static <T> FilterExpression<T> and(FilterExpression<T> left, FilterExpression<T> right) { return new AndExpression<>(left, right); }
public static <T> FilterExpression<T> or(FilterExpression<T> left, FilterExpression<T> right) { return new OrExpression<>(left, right); }
public static <T> FilterExpression<T> not(FilterExpression<T> expr) { return new NotExpression<>(expr); }}
// Using the Interpreterpublic record Product(String name, String category, double price, boolean active) {}
final var filter = new FilterBuilder<Product>();
// Build expression: category = 'Electronics' AND price > 100 AND active = truefinal FilterExpression<Product> expression = FilterBuilder.and( FilterBuilder.and( filter.where("category").equals("Electronics"), filter.where("price").greaterThan(100.0) ), filter.where("active").equals(true));
IO.println("Query: " + expression.toQueryString());// Output: ((category = 'Electronics' AND price > 100.0) AND active = true)
// Filter productsfinal List<Product> products = List.of( new Product("Laptop", "Electronics", 999.99, true), new Product("Mouse", "Electronics", 29.99, true), new Product("Desk", "Furniture", 199.99, true), new Product("Monitor", "Electronics", 299.99, false));
final var filtered = products.stream() .filter(p -> expression.interpret(new FilterContext<>(p, Map.of()))) .toList();
IO.println("Filtered: " + filtered);// Output: [Product[name=Laptop, category=Electronics, price=999.99, active=true]]Iterator
The Iterator pattern provides a way to access the elements of a collection sequentially without exposing its internal representation. It separates the responsibility of traversing the collection from the collection itself, allowing different iteration strategies.
This pattern offers flexibility to implement multiple ways of traversing a data structure. In modern Java, the pattern is built into the language through Iterable, Iterator, and the Streams API. However, it’s still valuable to implement it explicitly for custom data structures or when specialized iterators are needed.
Example: Custom iterators for binary tree
// Node structurepublic class TreeNode<T> { private final T value; private TreeNode<T> left; private TreeNode<T> right;
public TreeNode(T value) { this.value = value; }
public T value() { return value; } public TreeNode<T> left() { return left; } public TreeNode<T> right() { return right; }
public void setLeft(TreeNode<T> left) { this.left = left; } public void setRight(TreeNode<T> right) { this.right = right; }}
// Binary tree with multiple iteration strategiespublic class BinaryTree<T> implements Iterable<T> {
private TreeNode<T> root;
public enum TraversalOrder { IN_ORDER }
private TraversalOrder defaultOrder = TraversalOrder.IN_ORDER;
public void setRoot(TreeNode<T> root) { this.root = root; }
public void setDefaultTraversal(TraversalOrder order) { this.defaultOrder = order; }
@Override public Iterator<T> iterator() { return iterator(defaultOrder); }
public Iterator<T> iterator(TraversalOrder order) { return switch (order) { case IN_ORDER -> new InOrderIterator<>(root); }; }
public Iterable<T> inOrder() { return () -> iterator(TraversalOrder.IN_ORDER); }
// Stream support public Stream<T> stream() { return stream(defaultOrder); }
public Stream<T> stream(TraversalOrder order) { return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator(order), Spliterator.ORDERED), false ); }}
// In-Order iterator (left -> root -> right)class InOrderIterator<T> implements Iterator<T> { private final Deque<TreeNode<T>> stack = new ArrayDeque<>();
InOrderIterator(TreeNode<T> root) { pushLeft(root); }
private void pushLeft(TreeNode<T> node) { while (node != null) { stack.push(node); node = node.left(); } }
@Override public boolean hasNext() { return !stack.isEmpty(); }
@Override public T next() { if (!hasNext()) throw new NoSuchElementException(); TreeNode<T> current = stack.pop(); pushLeft(current.right()); return current.value(); }}
// Using the Iterator patternfinal var tree = new BinaryTree<Integer>();// 4// / \// 2 6// / \ / \// 1 3 5 7
final var root = new TreeNode<>(4);root.setLeft(new TreeNode<>(2));root.setRight(new TreeNode<>(6));root.left().setLeft(new TreeNode<>(1));root.left().setRight(new TreeNode<>(3));root.right().setLeft(new TreeNode<>(5));root.right().setRight(new TreeNode<>(7));tree.setRoot(root);
// Different ways to iterateIO.println("In-Order: " + tree.stream(TraversalOrder.IN_ORDER).toList());// [1, 2, 3, 4, 5, 6, 7]
// Using for-eachfor (Integer value : tree.inOrder()) { System.out.print(value + " ");}
// Using Streams for functional operationsint sum = tree.stream() .filter(n -> n % 2 == 0) .mapToInt(Integer::intValue) .sum();Mediator
The Mediator pattern defines an object that encapsulates how a set of objects interact. It promotes loose coupling by avoiding explicit references between objects, and allows their interactions to vary independently.
This pattern centralizes control of complex communication between related objects. Participating objects (colleagues) only know the mediator, not the other objects they interact with. This simplifies maintenance and makes it easier to reuse individual objects.
Example: Chat room as mediator
// Mediator interfacepublic interface ChatMediator { void sendMessage(String message, User sender); void sendPrivateMessage(String message, User sender, User recipient); void addUser(User user); void removeUser(User user); List<User> getOnlineUsers();}
// Colleague interface (user)public abstract class User { protected final String id; protected final String name; protected ChatMediator mediator; protected boolean online;
protected User(String id, String name) { this.id = id; this.name = name; this.online = false; }
public String id() { return id; } public String name() { return name; } public boolean isOnline() { return online; }
public void join(ChatMediator mediator) { this.mediator = mediator; this.online = true; mediator.addUser(this); }
public void leave() { if (mediator != null) { mediator.removeUser(this); this.online = false; } }
public abstract void send(String message); public abstract void sendPrivate(String message, User recipient); public abstract void receive(String message, User sender); public abstract void receivePrivate(String message, User sender);}
// Concrete user implementationpublic class ChatUser extends User { private final List<ChatMessage> messageHistory = new ArrayList<>();
public record ChatMessage(String content, String senderId, Instant timestamp, boolean isPrivate) {}
public ChatUser(String id, String name) { super(id, name); }
@Override public void send(String message) { if (mediator != null && online) { IO.println("[" + name + "] sends: " + message); mediator.sendMessage(message, this); } }
@Override public void sendPrivate(String message, User recipient) { if (mediator != null && online) { IO.println("[" + name + "] whispers to [" + recipient.name() + "]: " + message); mediator.sendPrivateMessage(message, this, recipient); } }
@Override public void receive(String message, User sender) { var chatMessage = new ChatMessage(message, sender.id(), Instant.now(), false); messageHistory.add(chatMessage); IO.println(" [" + name + "] received from [" + sender.name() + "]: " + message); }
@Override public void receivePrivate(String message, User sender) { var chatMessage = new ChatMessage(message, sender.id(), Instant.now(), true); messageHistory.add(chatMessage); IO.println(" [" + name + "] received private from [" + sender.name() + "]: " + message); }
public List<ChatMessage> getHistory() { return Collections.unmodifiableList(messageHistory); }}
// Mediator implementationpublic class ChatRoom implements ChatMediator { private final String roomId; private final String roomName; private final Set<User> users = ConcurrentHashMap.newKeySet(); private final List<BroadcastMessage> broadcastHistory = new ArrayList<>(); private final Map<String, List<String>> blockedUsers = new ConcurrentHashMap<>();
public record BroadcastMessage(String content, String senderId, Instant timestamp) {}
public ChatRoom(String roomId, String roomName) { this.roomId = roomId; this.roomName = roomName; }
@Override public void sendMessage(String message, User sender) { broadcastHistory.add(new BroadcastMessage(message, sender.id(), Instant.now()));
// The mediator coordinates who receives the message users.stream() .filter(user -> !user.equals(sender)) .filter(User::isOnline) .filter(user -> !isBlocked(sender.id(), user.id())) .forEach(user -> user.receive(message, sender)); }
@Override public void sendPrivateMessage(String message, User sender, User recipient) { if (users.contains(recipient) && recipient.isOnline() && !isBlocked(sender.id(), recipient.id())) { recipient.receivePrivate(message, sender); } }
@Override public void addUser(User user) { users.add(user); // Notify other users sendSystemMessage(user.name() + " has joined the chat"); }
@Override public void removeUser(User user) { users.remove(user); sendSystemMessage(user.name() + " has left the chat"); }
@Override public List<User> getOnlineUsers() { return users.stream().filter(User::isOnline).toList(); }
public void blockUser(String blockerId, String blockedId) { blockedUsers.computeIfAbsent(blockerId, k -> new ArrayList<>()).add(blockedId); }
private boolean isBlocked(String senderId, String recipientId) { return blockedUsers.getOrDefault(recipientId, List.of()).contains(senderId); }
private void sendSystemMessage(String message) { users.stream() .filter(User::isOnline) .forEach(user -> IO.println(" [SYSTEM -> " + user.name() + "]: " + message)); }}
// Using the Mediator patternvar chatRoom = new ChatRoom("room-1", "General");
var alice = new ChatUser("1", "Alice");var bob = new ChatUser("2", "Bob");var charlie = new ChatUser("3", "Charlie");
// Users join through the mediatoralice.join(chatRoom);bob.join(chatRoom);charlie.join(chatRoom);
// Mediated communicationalice.send("Hello everyone!");// [Alice] sends: Hello everyone!// [Bob] received from [Alice]: Hello everyone!// [Charlie] received from [Alice]: Hello everyone!
bob.sendPrivate("Hi Alice, how are you?", alice);// [Bob] whispers to [Alice]: Hi Alice, how are you?// [Alice] received private from [Bob]: Hi Alice, how are you?
// Block userchatRoom.blockUser("Charlie", "Bob");bob.send("This message will not reach Charlie");
alice.leave();// [SYSTEM -> Bob]: Alice has left the chat// [SYSTEM -> Charlie]: Alice has left the chatMemento
The Memento pattern allows capturing and externalizing an object’s internal state without violating its encapsulation, so it can be restored to that state later. It’s especially useful for implementing undo, checkpoints, or error recovery.
The pattern involves three participants: the Originator (the object whose state is saved), the Memento (stores the state), and the Caretaker (manages mementos without accessing their content). The Memento has a narrow interface for the Caretaker but a wide one for the Originator.
Example: Code editor with snapshots
// Memento - stores statepublic final class EditorMemento { private final String content; private final int cursorPosition; private final Set<Integer> breakpoints; private final Instant savedAt; private final String description;
// Package-private constructor - only Originator can create mementos EditorMemento(String content, int cursorPosition, Set<Integer> breakpoints, String description) { this.content = content; this.cursorPosition = cursorPosition; this.breakpoints = Set.copyOf(breakpoints); this.savedAt = Instant.now(); this.description = description; }
// Limited public interface for the Caretaker public Instant savedAt() { return savedAt; } public String description() { return description; }
// Package-private methods for the Originator String content() { return content; } int cursorPosition() { return cursorPosition; } Set<Integer> breakpoints() { return breakpoints; }}
// Originator - the editorpublic class CodeEditor { private StringBuilder content; private int cursorPosition; private final Set<Integer> breakpoints; private String filename;
public CodeEditor() { this.content = new StringBuilder(); this.cursorPosition = 0; this.breakpoints = new HashSet<>(); }
public void newFile(String filename) { this.filename = filename; this.content = new StringBuilder(); this.cursorPosition = 0; this.breakpoints.clear(); }
public void type(String text) { content.insert(cursorPosition, text); cursorPosition += text.length(); }
public void delete(int count) { int start = Math.max(0, cursorPosition - count); content.delete(start, cursorPosition); cursorPosition = start; }
public void moveCursor(int position) { this.cursorPosition = Math.max(0, Math.min(position, content.length())); }
public void toggleBreakpoint(int line) { if (breakpoints.contains(line)) { breakpoints.remove(line); } else { breakpoints.add(line); } }
public String getContent() { return content.toString(); }
// Create memento public EditorMemento save(String description) { return new EditorMemento( content.toString(), cursorPosition, breakpoints, description ); }
// Restore from memento public void restore(EditorMemento memento) { this.content = new StringBuilder(memento.content()); this.cursorPosition = memento.cursorPosition(); this.breakpoints.clear(); this.breakpoints.addAll(memento.breakpoints()); }
public void displayStatus() { IO.println("=== Editor Status ==="); IO.println("File: " + filename); IO.println("Content: " + content); IO.println("Cursor: " + cursorPosition); IO.println("Breakpoints: " + breakpoints); IO.println("===================="); }}
// Caretaker - manages memento historypublic class EditorHistory { private final Deque<EditorMemento> history = new ArrayDeque<>(); private final Deque<EditorMemento> redoStack = new ArrayDeque<>(); private final int maxSnapshots;
public EditorHistory(int maxSnapshots) { this.maxSnapshots = maxSnapshots; }
public void save(EditorMemento memento) { history.push(memento); redoStack.clear();
while (history.size() > maxSnapshots) { history.removeLast(); } }
public Optional<EditorMemento> undo() { if (history.size() > 1) { EditorMemento current = history.pop(); redoStack.push(current); return Optional.of(history.peek()); } return Optional.empty(); }
public Optional<EditorMemento> redo() { if (!redoStack.isEmpty()) { EditorMemento memento = redoStack.pop(); history.push(memento); return Optional.of(memento); } return Optional.empty(); }
public List<String> getSnapshotDescriptions() { return history.stream() .map(m -> "[%s] %s".formatted( m.savedAt().toString().substring(11, 19), m.description())) .toList(); }
public Optional<EditorMemento> getSnapshot(int index) { return history.stream().skip(index).findFirst(); }}
// Facade to simplify usagepublic class EditorWithHistory { private final CodeEditor editor; private final EditorHistory history;
public EditorWithHistory() { this.editor = new CodeEditor(); this.history = new EditorHistory(50); }
public void newFile(String filename) { editor.newFile(filename); saveSnapshot("New file: " + filename); }
public void type(String text) { editor.type(text); saveSnapshot("Typed: " + truncate(text, 20)); }
public void delete(int count) { editor.delete(count); saveSnapshot("Deleted " + count + " chars"); }
public void undo() { history.undo().ifPresent(memento -> { editor.restore(memento); IO.println("Undo: " + memento.description()); }); }
public void redo() { history.redo().ifPresent(memento -> { editor.restore(memento); IO.println("Redo: " + memento.description()); }); }
public void showHistory() { IO.println("=== Snapshot History ==="); history.getSnapshotDescriptions().forEach(System.out::println); }
private void saveSnapshot(String description) { history.save(editor.save(description)); }
private String truncate(String s, int maxLen) { return s.length() > maxLen ? s.substring(0, maxLen) + "..." : s; }
public String getContent() { return editor.getContent(); } public void displayStatus() { editor.displayStatus(); }}
// Using the Memento patternvar editor = new EditorWithHistory();
editor.newFile("Main.java");editor.type("public class Main {\n");editor.type(" public static void main(String[] args) {\n");editor.type(" IO.println(\"Hello\");\n");editor.type(" }\n");editor.type("}");
IO.println(editor.getContent());
editor.showHistory();
// Undo changeseditor.undo();editor.undo();IO.println("After 2 undos:\n" + editor.getContent());
// Redoeditor.redo();IO.println("After redo:\n" + editor.getContent());Observer
The Observer pattern defines a one-to-many relationship between objects so that when one object (subject) changes state, all its dependents (observers) are notified and updated automatically. Also known as publish-subscribe, this pattern is fundamental for implementing decoupled event systems.
The subject maintains a list of observers without knowing their concrete classes, providing maximum flexibility. Observers can subscribe, unsubscribe, or be notified of specific changes. An object can act simultaneously as subject and observer of other objects.
Example: Reactive event system
// Typed eventspublic interface DomainEvent { String eventId(); Instant occurredAt(); String aggregateId();}
public record OrderCreated( String eventId, Instant occurredAt, String aggregateId, String customerId, List<OrderItem> items, Money total) implements DomainEvent {}
public record OrderShipped( String eventId, Instant occurredAt, String aggregateId, String trackingNumber, String carrier) implements DomainEvent {}
// Observer interface with generic support@FunctionalInterfacepublic interface EventListener<E extends DomainEvent> { void onEvent(E event);}
// Subject - central EventBuspublic class EventBus {
private final Map<Class<? extends DomainEvent>, Set<EventListener<?>>> listeners = new ConcurrentHashMap<>();
private final ExecutorService asyncExecutor = Executors.newVirtualThreadPerTaskExecutor();
@SuppressWarnings("unchecked") public <E extends DomainEvent> Subscription subscribe(Class<E> eventType, EventListener<E> listener) { listeners.computeIfAbsent(eventType, k -> ConcurrentHashMap.newKeySet()) .add(listener);
return () -> unsubscribe(eventType, listener); }
public <E extends DomainEvent> void unsubscribe(Class<E> eventType, EventListener<E> listener) { var eventListeners = listeners.get(eventType); if (eventListeners != null) { eventListeners.remove(listener); } }
@SuppressWarnings("unchecked") public <E extends DomainEvent> void publish(E event) { var eventListeners = listeners.get(event.getClass()); if (eventListeners != null) { eventListeners.forEach(listener -> ((EventListener<E>) listener).onEvent(event)); } }
@SuppressWarnings("unchecked") public <E extends DomainEvent> void publishAsync(E event) { var eventListeners = listeners.get(event.getClass()); if (eventListeners != null) { eventListeners.forEach(listener -> asyncExecutor.submit(() -> { try { ((EventListener<E>) listener).onEvent(event); } catch (Exception e) { System.err.println("Error handling event: " + e.getMessage()); } })); } }
// Functional interface for unsubscribe @FunctionalInterface public interface Subscription extends AutoCloseable { void unsubscribe();
@Override default void close() { unsubscribe(); } }}
// Concrete observerspublic class InventoryService implements EventListener<OrderCreated> {
@Override public void onEvent(OrderCreated event) { IO.println("📦 Reserving inventory for order: " + event.aggregateId()); event.items().forEach(item -> IO.println(" - Reserving " + item.quantity() + " x " + item.productId())); }}
public class AnalyticsService {
private final Map<String, AtomicInteger> ordersByCustomer = new ConcurrentHashMap<>(); private final AtomicLong totalRevenue = new AtomicLong();
public void trackOrderCreated(OrderCreated event) { ordersByCustomer.computeIfAbsent(event.customerId(), k -> new AtomicInteger()) .incrementAndGet(); IO.println("📊 Analytics: New order tracked for customer " + event.customerId()); }}
// Other observer implementations...
// Using the Observer patternfinal var eventBus = new EventBus();
// Register observersfinal var emailSender = new EmailSender();final var notificationService = new NotificationService(emailSender);final var inventoryService = new InventoryService();final var analyticsService = new AnalyticsService();final var shippingNotifier = new ShippingNotifier();
// Subscriptions - different observers for the same eventfinal var sub1 = eventBus.subscribe(OrderCreated.class, notificationService);final var sub2 = eventBus.subscribe(OrderCreated.class, inventoryService);final var sub3 = eventBus.subscribe(OrderCreated.class, analyticsService::trackOrderCreated);final var sub4 = eventBus.subscribe(OrderShipped.class, shippingNotifier);
// Publish eventsfinal var orderCreated = new OrderCreated( UUID.randomUUID().toString(), Instant.now(), "ORD-001", "CUST-123", List.of(new OrderItem("PROD-A", 2), new OrderItem("PROD-B", 1)), new Money(299.99, "USD"));
eventBus.publish(orderCreated);// Output:// 📧 Sending order confirmation for order: ORD-001// 📦 Reserving inventory for order: ORD-001// - Reserving 2 x PROD-A// - Reserving 1 x PROD-B// 📊 Analytics: New order tracked for customer CUST-123
// Unsubscribesub1.unsubscribe();// Or using try-with-resourcestry (var subscription = eventBus.subscribe(OrderCreated.class, e -> IO.println("Temporary listener: " + e.aggregateId()))) { eventBus.publish(orderCreated);}State
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change class, since each state encapsulates the specific behavior associated with it in a separate class.
This pattern eliminates lengthy conditional statements that check the object’s current state. Instead, each possible state is represented as a class that implements the appropriate behavior. The context delegates operations to the current state object, and transitions between states can be managed by the context or by the states themselves.
Example: State machine for order process
// State interfacepublic sealed interface OrderState permits DraftState, PendingPaymentState, PaidState, ShippedState, DeliveredState, CancelledState {
void addItem(OrderContext order, OrderItem item); void removeItem(OrderContext order, String productId); void submitOrder(OrderContext order); void processPayment(OrderContext order, PaymentInfo payment); void shipOrder(OrderContext order, ShippingInfo shipping); void deliverOrder(OrderContext order); void cancelOrder(OrderContext order, String reason);
String stateName();
// Default behavior - operation not allowed default void throwInvalidOperation(String operation) { throw new IllegalStateException( "Cannot " + operation + " in state: " + stateName()); }}
// Concrete statespublic final class DraftState implements OrderState { public static final DraftState INSTANCE = new DraftState(); private DraftState() {}
@Override public String stateName() { return "DRAFT"; }
@Override public void addItem(OrderContext order, OrderItem item) { order.getItems().add(item); IO.println("✓ Item added: " + item); }
@Override public void removeItem(OrderContext order, String productId) { order.getItems().removeIf(i -> i.productId().equals(productId)); IO.println("✓ Item removed: " + productId); }
@Override public void submitOrder(OrderContext order) { if (order.getItems().isEmpty()) { throw new IllegalStateException("Cannot submit empty order"); } order.setState(PendingPaymentState.INSTANCE); IO.println("✓ Order submitted, awaiting payment"); }
@Override public void processPayment(OrderContext order, PaymentInfo payment) { throwInvalidOperation("process payment"); }
@Override public void shipOrder(OrderContext order, ShippingInfo shipping) { throwInvalidOperation("ship"); }
@Override public void deliverOrder(OrderContext order) { throwInvalidOperation("deliver"); }
@Override public void cancelOrder(OrderContext order, String reason) { order.setState(CancelledState.INSTANCE); order.setCancellationReason(reason); IO.println("✓ Draft order cancelled: " + reason); }}
public final class PendingPaymentState implements OrderState { public static final PendingPaymentState INSTANCE = new PendingPaymentState(); private PendingPaymentState() {}
@Override public String stateName() { return "PENDING_PAYMENT"; }
@Override public void addItem(OrderContext order, OrderItem item) { throwInvalidOperation("add items"); }
@Override public void removeItem(OrderContext order, String productId) { throwInvalidOperation("remove items"); }
@Override public void submitOrder(OrderContext order) { throwInvalidOperation("submit"); }
@Override public void processPayment(OrderContext order, PaymentInfo payment) { // Validate and process payment if (payment.amount().compareTo(order.getTotal()) < 0) { throw new IllegalArgumentException("Insufficient payment amount"); } order.setPaymentInfo(payment); order.setState(PaidState.INSTANCE); IO.println("✓ Payment processed: " + payment.amount()); }
@Override public void shipOrder(OrderContext order, ShippingInfo shipping) { throwInvalidOperation("ship"); }
@Override public void deliverOrder(OrderContext order) { throwInvalidOperation("deliver"); }
@Override public void cancelOrder(OrderContext order, String reason) { order.setState(CancelledState.INSTANCE); order.setCancellationReason(reason); IO.println("✓ Order cancelled before payment: " + reason); }}
// Other state implementations
// Context@Getter@Setterpublic class OrderContext { private final String orderId; private final List<OrderItem> items = new ArrayList<>(); private OrderState state = DraftState.INSTANCE; private PaymentInfo paymentInfo; private ShippingInfo shippingInfo; private String cancellationReason; private boolean refundRequired; private Instant deliveredAt;
public OrderContext(String orderId) { this.orderId = orderId; }
// Delegation to current state public void addItem(OrderItem item) { state.addItem(this, item); } public void removeItem(String productId) { state.removeItem(this, productId); } public void submit() { state.submitOrder(this); } public void pay(PaymentInfo payment) { state.processPayment(this, payment); } public void ship(ShippingInfo shipping) { state.shipOrder(this, shipping); } public void deliver() { state.deliverOrder(this); } public void cancel(String reason) { state.cancelOrder(this, reason); }
public Money getTotal() { return items.stream() .map(i -> i.price().multiply(i.quantity())) .reduce(Money.ZERO, Money::add); }
public void printStatus() { IO.println("\n=== Order " + orderId + " ==="); IO.println("State: " + state.stateName()); IO.println("Items: " + items); IO.println("Total: " + getTotal()); }}
// Using the State patternfinal var order = new OrderContext("ORD-001");
order.addItem(new OrderItem("LAPTOP", 1, new Money(999.99, "USD")));order.addItem(new OrderItem("MOUSE", 2, new Money(29.99, "USD")));order.printStatus(); // State: DRAFT
order.submit(); // ✓ Order submitted, awaiting paymentorder.printStatus(); // State: PENDING_PAYMENT
order.pay(new PaymentInfo("4111111111111111", new Money(1059.97, "USD")));// ✓ Payment processed
order.ship(new ShippingInfo("TRACK-123", "FedEx"));// ✓ Order shipped
order.deliver();// ✓ Order delivered!
order.printStatus(); // State: DELIVERED
// Attempt invalid operationtry { order.cancel("Changed my mind");} catch (IllegalStateException e) { IO.println("Error: " + e.getMessage()); // Error: Cannot cancel delivered in state: DELIVERED}Strategy
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently from the clients that use it. This pattern is ideal when you have multiple ways to perform a task and the choice of algorithm must be made at runtime.
Unlike the State pattern where behavior changes according to internal state, in Strategy the choice of algorithm depends on external conditions such as configuration, user preferences, or data type. In modern Java, strategies can be implemented elegantly using functional interfaces and lambdas.
Example: Payment processing system with strategies
// Strategy interface@FunctionalInterfacepublic interface PricingStrategy extends Function<Order, Money> {
default String strategyName() { return this.getClass().getSimpleName(); }}
// Concrete strategiespublic class StandardPricing implements PricingStrategy { @Override public Money apply(Order order) { return order.subtotal(); }
@Override public String strategyName() { return "Standard Pricing"; }}
public class MembershipPricing implements PricingStrategy { private final MembershipLevel level;
@Getter @RequiredArgsConstructor public enum MembershipLevel { SILVER(0.05), GOLD(0.10), PLATINUM(0.15);
private final double discount; }
public MembershipPricing(MembershipLevel level) { this.level = level; }
@Override public Money apply(Order order) { return order.subtotal().multiply(1 - level.getDiscount()); }
@Override public String strategyName() { return level + " Membership (" + (int)(level.getDiscount() * 100) + "% off)"; }}
// Composite strategy - applies multiple strategiespublic class CompositePricingStrategy implements PricingStrategy { private final List<PricingStrategy> strategies; private final CombinationMode mode;
public enum CombinationMode { BEST_PRICE, STACK_ALL }
public CompositePricingStrategy(CombinationMode mode, PricingStrategy... strategies) { this.mode = mode; this.strategies = List.of(strategies); }
@Override public Money apply(Order order) { return switch (mode) { case BEST_PRICE -> strategies.stream() .map(s -> s.apply(order)) .min(Comparator.naturalOrder()) .orElse(order.subtotal()); case STACK_ALL -> { Money current = order.subtotal(); for (var strategy : strategies) { Order tempOrder = order.withSubtotal(current); current = strategy.apply(tempOrder); } yield current; } }; }
@Override public String strategyName() { return "Composite: " + mode; }}
// Usage with lambdas - strategies as functions@RequiredArgsConstructorpublic class FunctionalPricingExample {
public Money applyDiscount(Order order, PricingStrategy strategy) { return strategy.apply(order); }
public static void main(String[] args) { var example = new FunctionalPricingExample(); var order = new Order("ORD-001", List.of( new OrderItem("PROD-A", 5, new Money(20, "USD")), new OrderItem("PROD-B", 3, new Money(15, "USD")) ));
// Strategies as lambdas PricingStrategy standard = new StandardPricing(); PricingStrategy members = new MembershipPricing(MembershipLevel.GOLD);
IO.println("Standard discount: " + example.applyDiscount(order, standard)); IO.println("10% off for members: " + example.applyDiscount(order, members)); IO.println("Compound: " + example.applyDiscount(order, new CompositePricingStrategy(CombinationMode.BEST_PRICE, standard, members))); }}Template Method
The Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. It lets subclasses redefine certain steps of the algorithm without changing its overall structure. The philosophy is “Don’t call us, we’ll call you” (Hollywood Principle).
The base class defines the sequence of steps and provides default or abstract implementations for each step. Subclasses can override specific steps while the algorithm’s structure remains intact. “Hooks” can also be included—empty methods that subclasses can optionally override.
Example: Data processing pipeline
// Abstract templatepublic abstract class DataProcessor<T, R> {
// Template method - defines the algorithm structure public final ProcessingResult<R> process(DataSource<T> source) { final var startTime = Instant.now(); final var metrics = new ProcessingMetrics();
try { // Hook - optional preparation beforeProcessing(source);
// Step 1: Extract data (abstract) final List<T> rawData = extractData(source); metrics.setRecordsRead(rawData.size());
// Step 2: Validate data (with default implementation) final List<T> validData = validateData(rawData, metrics);
// Step 3: Transform data (abstract) final List<R> transformedData = transformData(validData); metrics.setRecordsTransformed(transformedData.size());
// Step 4: Filter data (with default implementation) final List<R> filteredData = filterData(transformedData); metrics.setRecordsFiltered(transformedData.size() - filteredData.size());
// Step 5: Load/save data (abstract) final int loaded = loadData(filteredData); metrics.setRecordsLoaded(loaded);
// Hook - optional completion afterProcessing(metrics);
metrics.setDuration(Duration.between(startTime, Instant.now())); return ProcessingResult.success(filteredData, metrics);
} catch (Exception e) { handleError(e, metrics); metrics.setDuration(Duration.between(startTime, Instant.now())); return ProcessingResult.failure(e.getMessage(), metrics); } }
// Abstract methods - subclasses MUST implement protected abstract List<T> extractData(DataSource<T> source); protected abstract List<R> transformData(List<T> data); protected abstract int loadData(List<R> data);
// Methods with default implementation - subclasses MAY override protected List<T> validateData(List<T> data, ProcessingMetrics metrics) { // By default, all data is valid return data; }
protected List<R> filterData(List<R> data) { // By default, nothing is filtered return data; }
// Hooks - empty methods that subclasses can override protected void beforeProcessing(DataSource<T> source) { // Empty hook by default }
protected void afterProcessing(ProcessingMetrics metrics) { // Empty hook by default }
protected void handleError(Exception e, ProcessingMetrics metrics) { System.err.println("Error processing: " + e.getMessage()); metrics.setError(e.getMessage()); }}
// Concrete implementation: API to Database processor@RequiredArgsConstructorpublic class ApiToDatabaseProcessor extends DataProcessor<ApiResponse, DatabaseRecord> {
private final DatabaseConnection db; private final String tableName;
@Override protected List<ApiResponse> extractData(DataSource<ApiResponse> source) { return source.readAll(); }
@Override protected List<DatabaseRecord> transformData(List<ApiResponse> data) { return data.stream() .map(response -> new DatabaseRecord( response.id(), response.data(), Instant.now() )) .toList(); }
@Override protected int loadData(List<DatabaseRecord> data) { return db.batchInsert(tableName, data); }
@Override protected void handleError(Exception e, ProcessingMetrics metrics) { // More detailed logging for DB errors System.err.println("Database error: " + e.getMessage()); db.rollback(); super.handleError(e, metrics); }}
// Using the Template Methodfinal var apiToDbProcessor = new ApiToDatabaseProcessor(posgresqlConn, "processingTable");
final var result = apiToDbProcessor.process(apiResponse);
if (result.success()) { IO.println("Created %d records".formatted(result.data().size()));} else { System.err.println("Processing failed: " + result.errorMessage());}Visitor
The Visitor pattern allows defining new operations on a structure of objects without modifying the classes of the elements it operates on. It separates an algorithm from the object structure it operates on, making it easy to add new operations without changing existing classes.
This pattern is ideal when the object structure is stable but the operations on it change frequently. It defines double dispatch: the element accepts a visitor and the visitor determines which operation to run based on the concrete type of the element.
Example: Document reporting system
// Visitor interfacepublic interface DocumentVisitor<T> { T visitParagraph(Paragraph paragraph); T visitHeading(Heading heading); T visitCodeBlock(CodeBlock codeBlock);
// Default method for composite elements default T visitDocument(Document document) { document.elements().forEach(e -> e.accept(this)); return null; }}
// Element interfacepublic interface DocumentElement { <T> T accept(DocumentVisitor<T> visitor);}
// Concrete elementspublic record Paragraph(String text, TextStyle style) implements DocumentElement { public enum TextStyle { NORMAL, BOLD, ITALIC, QUOTE }
@Override public <T> T accept(DocumentVisitor<T> visitor) { return visitor.visitParagraph(this); }}
public record Heading(String text, int level) implements DocumentElement { public Heading { if (level < 1 || level > 6) throw new IllegalArgumentException("Level must be 1-6"); }
@Override public <T> T accept(DocumentVisitor<T> visitor) { return visitor.visitHeading(this); }}
public record CodeBlock(String code, String language) implements DocumentElement { @Override public <T> T accept(DocumentVisitor<T> visitor) { return visitor.visitCodeBlock(this); }}
public record Document(String title, List<DocumentElement> elements) implements DocumentElement { @Override public <T> T accept(DocumentVisitor<T> visitor) { return visitor.visitDocument(this); }}
// Concrete visitor: Export to Markdownpublic class MarkdownExportVisitor implements DocumentVisitor<String> {
private final StringBuilder markdown = new StringBuilder();
@Override public String visitParagraph(Paragraph paragraph) { String result = switch (paragraph.style()) { case NORMAL -> paragraph.text(); case BOLD -> "**" + paragraph.text() + "**"; case ITALIC -> "*" + paragraph.text() + "*"; case QUOTE -> "> " + paragraph.text(); }; markdown.append(result).append("\n\n"); return result; }
@Override public String visitHeading(Heading heading) { String result = "#".repeat(heading.level()) + " " + heading.text(); markdown.append(result).append("\n\n"); return result; }
@Override public String visitCodeBlock(CodeBlock codeBlock) { String result = "```%s\n%s\n```".formatted(codeBlock.language(), codeBlock.code()); markdown.append(result).append("\n\n"); return result; }
@Override public String visitDocument(Document document) { markdown.append("# ").append(document.title()).append("\n\n"); document.elements().forEach(e -> e.accept(this)); return markdown.toString(); }
public String getMarkdown() { return markdown.toString(); }}
// Using the Visitor patternfinal var document = new Document("My Document", List.of( new Heading("Introduction", 1), new Paragraph("This is an example document.", Paragraph.TextStyle.NORMAL), new Heading("Data", 2), new Heading("Code", 2), new CodeBlock("IO.println(\"Hello!\");", "java")));
// Export to Markdownfinal var markdownVisitor = new MarkdownExportVisitor();document.accept(markdownVisitor);IO.println(markdownVisitor.getMarkdown());