Patterns in Modern Java
Purpose
Separate what a pattern is from how it is written. Almost no GoF pattern has become obsolete; many have stopped needing a class hierarchy, and several are now supplied by the language or the framework so completely that hand-writing them duplicates working machinery.
The distinction to hold throughout:
Design intent "this algorithm varies, and callers must not know
which one runs" → still true, still Strategy
Implementation interface + N classes + a selector
mechanism → optional; a lambda expresses the same intent
Refusing the vocabulary because the mechanism changed is as costly as building the 1994 mechanism. Say "Strategy, as a function" and the design stays legible.
Inspect compiler release/toolchains, resolved framework versions, CI/runtime and existing extension contracts before suggesting a replacement. Records/sealed types fit Java 17; record patterns and pattern switch need Java 21 without preview; Gatherers are final in 24 and ScopedValue in 25. StructuredTaskScope remains preview in Java 25: its release-specific API needs explicit project preview authorization and compiler/runtime flags. Do not upgrade or enable preview merely to use this skill. Missing environment evidence makes a version-sensitive recommendation conditional.
Three categories
COMMON MECHANISMS PROVIDED — reuse them when their guarantees match
Iterator Iterable / Iterator / Spliterator (Stream is a pipeline, not a replacement)
Singleton-like lifecycle the container's singleton scope (not global uniqueness)
Proxy @Transactional, @Cacheable, JPA lazy loading
Decorator servlet filters, interceptors, client builders
Observer application events; reactive streams; brokers
Chain of Resp. filter chains, interceptor chains
ALTERNATIVE EXPRESSIONS — compare with the existing contract
Strategy a lambda or a domain functional interface
Command a record; often a Runnable/Callable
Factory Method an injected Supplier or a keyed map
Visitor sealed interface + exhaustive switch
State sealed states + one transition function
Prototype a copy factory; usually immutability instead
Memento an immutable state behind one reference
Template Method a final class taking composed steps
Builder compare records/factories with staged construction needs
DOMAIN DESIGN STILL REQUIRED — libraries may supply mechanisms;
the classical questions apply with modern types
Composite, Bridge, Mediator, Interpreter, Abstract Factory,
Adapter, Facade, Flyweight
The two changes that matter most
Sealed types plus pattern matching. A closed hierarchy with
an exhaustive switch can express external operations and state dispatch. Adding an operation is
often one new function; adding a variant requires updating affected exhaustive switches on
recompilation. Catch-all cases and non-sealed branches limit that check. Sealing does not define
valid state transitions or decide which Composite types expose child mutation. Where you own
the relevant variants and compatibility boundary, consider it
(java-composition-over-inheritance).
The container. DI can own instance lifetimes and assemble a family, but singleton scope is per bean definition/container, profiles can overlap, and compatible-family invariants need tests or validation. Supplier injection can replace application-controlled creation hooks when public extension and lifecycle contracts permit it.
Two smaller but real ones: records remove the boilerplate that made Builder, Memento and
Prototype heavy, but records are only shallowly immutable. Virtual threads can simplify
blocking I/O orchestration; they do not remove requests represented as Commands, durable work,
admission control, cancellation or downstream capacity limits (thread-sizing-and-virtual-threads).
Decision rules
IF you are implementing a pattern from an older text
THEN check whether the JDK/framework supplies the required semantics before
hand-writing infrastructure. Do not turn categories into prohibitions.
IF the framework provides the mechanism
THEN compare ordering, lifecycle, failure and observability semantics before reuse.
Avoid duplicate policy; custom implementations can still integrate correctly.
IF the variant set is closed and you own it
THEN consider sealed + exhaustive switch against Visitor, State and Strategy,
preserving behavior, compatibility and the actual direction of change.
IF the variant set is open to code you do not compile
THEN preserve an extension interface. Classic Visitor also couples visitors to
element types; it does not automatically solve independently added variants.
IF a lambda would express the pattern
THEN compare a function with a named implementation for state, metadata,
checked failures and diagnostics; name the intent whichever form is chosen.
IF diagnosability matters (a hot path, a production stack trace)
THEN inspect actual traces/profiles; named methods/classes can help, but a lambda
is not automatically undiagnosable (flame-graph-analysis).
IF a pattern exists to defer or offload work
THEN check whether virtual threads remove the need
(thread-sizing-and-virtual-threads, structured-concurrency).
IF immutable context must flow down a bounded call tree
THEN consider ScopedValue on Java 25+. ThreadLocal remains appropriate for
mutable/per-thread integration in some libraries but requires lifecycle
cleanup and does not automatically propagate to arbitrary executor tasks
(scoped-values).
What has not changed
- Coupling analysis. Who knows whom, and what must not know what, is unaffected by syntax.
- The cost of indirection. A lambda hides a dispatch site exactly as a class does.
- Naming. "Strategy", "Adapter", "Mediator" still tell a reader what to expect, and the expectations differ.
- The high-risk set. Singleton, Observer, Mediator, Proxy, Flyweight and Prototype are risky for reasons the language does not address — global state, unspecified ordering, god objects, hidden network calls, shared mutation, and broken copying.
- "No pattern" as an answer. Modern features make it easier to reach, not less legitimate.
References
Return the existing mechanism, proposed replacement or reason to retain it, target compatibility, preserved contracts and checks executed versus pending. A shorter class list is not validation.
- Feature to pattern — each modern Java feature with the
patterns it changes and how: records, sealed types, pattern matching, switch expressions,
lambdas and method references, generics,
Optional, immutable collections, default methods, dependency injection, virtual threads, structured concurrency,ScopedValueand Gatherers. Read when a language feature suggests a design might be simplified. - Pattern by pattern — all twenty-three with the modern verdict, the mechanism that replaces or supplies each, and the residual case where the classical form is still correct. Read when implementing a specific pattern from an older reference.