LESSON 12 · BEGINNER4 min read

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.

OpenJDK 21+ (LTS)Free to readModule 3 of 15
YOUR NEXT SMALL WINS

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.

example.js☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

example.js☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

notes.txtPlain text
+--------------------+----------------------------+----------------------------------+| 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.

LET IT CLICK

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.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

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.

01

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.

your_turn_1.java☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.javaJava
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.

02

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.

your_turn_2.java☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.javaJava
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.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Why does modern Java eliminate the need for the classic GoF Visitor pattern?
02What is the primary benefit of a compact constructor in a Java record?
03What happens if a developer adds a new permitted record to a sealed interface when an existing switch expression does not have a default branch?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

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.

EXPECTED OUTPUT
Build successful. (Java 15)
[Process exited normally]
Main.javaJava
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

MODULE 03 · PRACTICAL SELF-CHECK

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 module

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.