Modern GoF Patterns: Builder, Factory, and Visitor via Pattern Matching
Re-architect classic Gang of Four design patterns using modern Java 21 idioms: immutable Records, Sealed class hierarchies, and exhaustive pattern-matching switches.
By the end, you’ll be able to…
- Implement fluent, type-safe Builder patterns using immutable record constructors
- Replace verbose classic Visitor patterns with exhaustive switch pattern matching over sealed types
- Construct modern Factory abstractions that enforce compile-time domain invariants
01Deconstructing the Classic Visitor Pattern
For decades, the classic Gang of Four Visitor pattern was the standard object-oriented solution for adding operations to composite object structures without modifying the elements themselves. However, classic Visitor requires double dispatch: every visited node must implement an accept(Visitor v) method, and every visitor interface must declare explicit visit(ConcreteNodeA) and visit(ConcreteNodeB) overloads.
This architectural ceremony introduces immense friction. Adding a new subtype requires editing every visitor interface and implementation across the entire codebase. Furthermore, passing contextual state through the visit traversal requires mutable accumulator fields inside the visitor instance, introducing subtle concurrency vulnerabilities.
Modern Java completely replaces the Visitor pattern with the combination of sealed interfaces and pattern-matching switch expressions. Because the compiler knows every possible implementation of a sealed type at compile time, a switch expression over the sealed root is provably exhaustive—eliminating both double dispatch and default fallback branches.
02Immutable Records with Fluent Builders
The Builder pattern remains essential when constructing complex domain objects with numerous optional parameters, validation constraints, and nested configuration options. However, writing traditional mutable builders with repetitive private fields, setters, and build() methods produces hundreds of lines of boilerplate.
In Java 21, we combine records with static builder classes. The record provides immutable value semantics, canonical constructors with compact validation, and automatic equals, hashCode, and toString implementations. The builder provides a fluent, step-wise configuration interface that enforces invariant validation before instantiation.
By placing validation logic inside the record's compact constructor, we guarantee that no code path can ever create an invalid instance—even if an attacker attempts reflection or deserialization bypasses.
03Architectural Comparison: Classic vs Modern Patterns
Let us examine how modern Java idioms transform classic architectural patterns across maintainability, type safety, and runtime characteristics:
Notice how modern patterns shift failure modes from runtime ClassCastExceptions and missing visit methods to compile-time verification errors.
+--------------------+----------------------------+----------------------------------+| Classic GoF | Legacy Java Approach | Modern Java 21 Idiom |+--------------------+----------------------------+----------------------------------+| Visitor | Interface + accept() | Sealed interface + switch record || Strategy | Concrete classes / anon | Lambdas / Method References || Builder | Mutable Java Beans | Record + static fluent builder || Factory Method | Reflection / string keys | Sealed interface factory methods || State Pattern | Mutable state reference | Sealed state machine transitions |+--------------------+----------------------------+----------------------------------+04Exhaustive State Machines with Sealed Transitions
Modeling state transitions using sealed interfaces guarantees that invalid transitions are rejected at compile time. In a payment processing gateway, for instance, an OrderState can be represented as a sealed interface permitted only by OrderPending, OrderAuthorized, OrderCaptured, and OrderRefunded.
When handling state transitions, a pure transition function takes the current state and incoming event, returning the new state. If a developer later introduces a new OrderChargeback state, every transition switch in the codebase will immediately produce a compilation error until explicitly handled.
This technique eliminates the classic enterprise bug where an unhandled state falls through an arbitrary if-else chain and silently leaves accounts in corrupted intermediate conditions.
The ideas worth keeping.
- Replace classic Visitor double-dispatch patterns with sealed interfaces and switch pattern matching.
- Combine records with compact constructors and fluent builders to achieve type-safe, validated immutability.
- Model state machines as sealed hierarchies to get compile-time verification across all business logic transitions.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Create a sealed interface named OrderCommand
Create a sealed interface named OrderCommand with records PlaceOrder(String orderId, double amount) and CancelOrder(String orderId, String reason). Write an exhaustive dispatch function using switch pattern matching.
Explore a worked solution
public class OrderProcessor { public sealed interface OrderCommand permits PlaceOrder, CancelOrder {} public record PlaceOrder(String orderId, double amount) implements OrderCommand {} public record CancelOrder(String orderId, String reason) implements OrderCommand {} public static String dispatch(OrderCommand cmd) { return switch (cmd) { case PlaceOrder(String id, double amt) -> "Placing order " + id + " for $" + amt; case CancelOrder(String id, String reason) -> "Canceling order " + id + ": " + reason; }; }}Use record deconstruction patterns inside the switch cases to extract components directly.
Exercise 2: Implement a compact constructor for an immuta
Implement a compact constructor for an immutable record AccountCredentials(String username, String passwordHash) that rejects null or blank inputs.
Explore a worked solution
public record AccountCredentials(String username, String passwordHash) { public AccountCredentials { if (username == null || username.isBlank()) { throw new IllegalArgumentException("Username must not be null or blank"); } if (passwordHash == null || passwordHash.length() < 32) { throw new IllegalArgumentException("Invalid password hash representation"); } }}In a compact constructor, omit the parameter list and perform validation directly on the record components.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Create a sealed interface named OrderCommand
Create a sealed interface named OrderCommand with records PlaceOrder(String orderId, double amount) and CancelOrder(String orderId, String reason). Write an exhaustive dispatch function using switch pattern matching.
Build successful. (Java 15) [Process exited normally]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
Pause. Put the pieces together.
You’ve reached the end of Modern OOP: Sealed Hierarchies, Records & Pattern Matching. Before moving on, use this task to check what you can do independently.
Refactor a legacy inheritance hierarchy into sealed interfaces with exhaustive pattern matching.
Try it in your own environment and write down the result. This is a self-check; reading completion does not evaluate your code.
Review this moduleOne lesson closer.
Reading is a start. Trying it yourself makes the connection.