Patterns and Modern Frameworks
Purpose
Answer two questions that cause a lot of wasted work: does the framework already do
this?, and what does this pattern look like in modern Java?
Both failures are common. Rebuilding a pattern the framework provides produces a wrapper
that is worse than what it wraps. Assuming a pattern is present when it is only partly
present produces a design that relies on a guarantee nobody makes — an identity map assumed
to be a cache, a unit of work assumed to span a request, a repository assumed to protect an
aggregate.
The map
Mechanism substantially provided — configure before rebuilding
Front Controller DispatcherServlet / router
Unit of Work JPA persistence context
Identity Map first-level cache
Lazy Load ORM proxies
Metadata Mapping JPA annotations / orm.xml
Template View Thymeleaf and friends
Plugin conditional bean registration
Registry the application context (used well: injection)
Provided, partial — the mechanism exists, the design decision does not
Repository Spring Data gives the implementation; the
aggregate boundary and the interface's shape
are still yours
Service Layer @Transactional gives demarcation; what a use
case is remains a design decision
Optimistic Offline Lock @Version detects; the conflict experience,
the retry policy and bulk-update safety are
yours
Data Mapper JPA maps; whether the domain may diverge from
the schema is your choice
Not provided — you must design it
Domain Model organisation, aggregate boundaries and invariants
Remote Facade granularity
Pessimistic Offline Lock across requests
Coarse-Grained Lock scope
Application Controller flows
Session state placement
Distribution boundaries and saga design
Workflow
- Before implementing a pattern, locate it in the map. If the mechanism exists, compare its
actual guarantees and extension points before wrapping or rebuilding it.
- For the second group, separate mechanism from decision. The framework supplies the
mechanism; the decision is still yours and is where the value is.
- Inspect the actual toolchain, dependencies and configuration: Java release, provider,
transaction manager, proxy/weaving mode, context lifetime and enhancement where relevant.
Do not infer guarantees from annotation names or upgrade to match an example.
- Express the pattern in modern Java where the language now does the work — records for
value objects and DTOs, sealed interfaces for closed hierarchies, exhaustive switch for
dispatch.
- Do not force a modern idiom where it changes the pattern's intent. A record cannot be
a mutable aggregate root; a sealed hierarchy is not a plugin point.
- When a pattern looks obsolete, separate the idea from its implementation. Most
classical patterns have been absorbed, not refuted — the idea still explains the
framework's behaviour.
Decision rules
The framework provides the pattern completely
→ configure it first; add a wrapper only for a demonstrated semantic boundary.
The framework provides the mechanism, you own the decision
→ make the decision explicitly and write it down. This is where
the pattern knowledge actually pays.
The framework provides something similar with different guarantees
→ read the guarantee. Context-local identity is not cross-context caching;
context lifetime is integration-dependent; bulk updates need explicit
version participation.
The framework does not provide it
→ design it, using the pattern as the starting point rather
than the answer (pattern-selection-and-composition).
A pattern's classical implementation conflicts with a modern idiom
→ keep the intent, change the implementation. Immutability,
records and sealed types usually express the intent better.
A pattern appears obsolete
→ check whether it was absorbed rather than refuted. Table
Module's idea survives as set-based SQL; a Row Data Gateway
must still own row persistence, not just carry projection data.
Rules
- Do not mechanically wrap a framework abstraction. A
CacheService over the
caching abstraction, a TransactionService over @Transactional, an HttpService over
RestClient — each adds a name, removes features, and will not survive replacing the
framework anyway unless it narrows capability, owns domain semantics, translates failures or
provides a genuine replacement/test seam (enterprise-architecture-smells).
- Spring Data does not decide your aggregate boundary. It generates an implementation.
Which aggregates exist, what the repository's surface is, and whether reads go through it
remain design decisions and are the whole content of the pattern
(
repository-pattern).
- A transaction-scoped persistence context is the common unit-of-work lifetime. Extended contexts
and Open Session In View can outlive one service transaction; evaluate their explicit consistency,
query and connection behavior rather than calling every longer scope inherently worse
(
orm-behavioral-patterns).
- The first-level cache provides context-local identity. It lasts with the persistence
context, which need not end at transaction completion. It does not provide freshness or
thread safety. Cross-context caching requires an explicit cache/provider and invalidation
contract (
caching-strategies).
@Version implements managed-entity conflict detection. The client's original version,
conflict presentation, valid retry and bulk SQL participation remain application concerns
(offline-concurrency-control).
- JPA does not require public JavaBean setters. Portable entities need a public/protected
no-arg constructor and a valid field or property access strategy. Field access supports
mutation through domain methods; persistent fields must not be final (
domain-logic-organization).
- Records are often effective for immutable values, DTOs, commands and events. They are not JPA
entities and do not fit aggregates that require in-place mutation/proxying, but aggregate state is
not mutable “by definition”; immutable replacement/event-sourced models exist.
- Sealed interfaces plus exhaustive
switch give a closed hierarchy with compile-checked
handling. That is better than a Special Case subclass where callers must distinguish, and
worse where they must not (enterprise-base-patterns).
- Virtual threads make thread-per-task blocking designs competitive for I/O-heavy Java services;
they do not make them a universal default. Pinning, native calls, downstream capacity, memory and
framework support still decide. They change none of
these patterns; what they change is the sizing arithmetic around them
(
thread-sizing-and-virtual-threads). A pattern that was chosen to avoid blocking a
platform thread may be worth revisiting; one chosen for a domain reason is not.
- A pattern absorbed by a framework is still worth understanding. The framework's
surprising behaviours are the pattern's classical consequences, and someone who knows the
pattern predicts them instead of debugging them.
For the proposed implementation, return the framework mechanism and its verified scope,
the application responsibility it leaves open, and a targeted test of the relevant gap
(for example rollback, context lifetime, stale-client writes or cache interception).
When configuration evidence is missing, state the assumption and how to verify it; do not
present the feature as an established guarantee. Keep the response proportional to the task.
References
- What the framework already provides — pattern by
pattern: what Spring and JPA implement, what they guarantee, the gap between the classical
pattern and the framework's version, and the wrapper to avoid in each case. Read before
implementing any classical pattern in a Spring stack.
- Modern Java expression — records, sealed types,
exhaustive switch, immutability and virtual threads applied to the enterprise patterns:
where they express the intent better, where they conflict with it, and the persistence
constraints that decide which. Read when writing a pattern in current Java, or when
modernising an old implementation.
1---2name: patterns-and-modern-frameworks3description: Which classical enterprise patterns a modern Java and Spring stack already implements, which it only partly implements, and which it does not implement at all — plus the modern Java expression of each. Use when a repository interface is written over Spring Data, when a unit of work or identity map is built over JPA, when a front controller is hand-rolled, when a caching layer is written over the caching abstraction, when an entity is written as a mutable bean because "JPA requires it", when a pattern's implementation is copied from an old text, or when deciding whether a pattern is obsolete or merely invisible. Does not cover choosing the pattern (pattern-selection-and-composition) or judging whether an abstraction should exist (enterprise-architecture-smells).4---56# Patterns and Modern Frameworks78## Purpose910Answer two questions that cause a lot of wasted work: **does the framework already do11this?**, and **what does this pattern look like in modern Java?**1213Both failures are common. Rebuilding a pattern the framework provides produces a wrapper14that is worse than what it wraps. Assuming a pattern is present when it is only partly15present produces a design that relies on a guarantee nobody makes — an identity map assumed16to be a cache, a unit of work assumed to span a request, a repository assumed to protect an17aggregate.1819## The map2021```text22Mechanism substantially provided — configure before rebuilding23 Front Controller DispatcherServlet / router24 Unit of Work JPA persistence context25 Identity Map first-level cache26 Lazy Load ORM proxies27 Metadata Mapping JPA annotations / orm.xml28 Template View Thymeleaf and friends29 Plugin conditional bean registration30 Registry the application context (used well: injection)3132Provided, partial — the mechanism exists, the design decision does not33 Repository Spring Data gives the implementation; the34 aggregate boundary and the interface's shape35 are still yours36 Service Layer @Transactional gives demarcation; what a use37 case is remains a design decision38 Optimistic Offline Lock @Version detects; the conflict experience,39 the retry policy and bulk-update safety are40 yours41 Data Mapper JPA maps; whether the domain may diverge from42 the schema is your choice4344Not provided — you must design it45 Domain Model organisation, aggregate boundaries and invariants46 Remote Facade granularity47 Pessimistic Offline Lock across requests48 Coarse-Grained Lock scope49 Application Controller flows50 Session state placement51 Distribution boundaries and saga design52```5354## Workflow55561. **Before implementing a pattern, locate it in the map.** If the mechanism exists, compare its57 actual guarantees and extension points before wrapping or rebuilding it.582. **For the second group, separate mechanism from decision.** The framework supplies the59 mechanism; the decision is still yours and is where the value is.603. **Inspect the actual toolchain, dependencies and configuration:** Java release, provider,61 transaction manager, proxy/weaving mode, context lifetime and enhancement where relevant.62 Do not infer guarantees from annotation names or upgrade to match an example.634. **Express the pattern in modern Java** where the language now does the work — records for64 value objects and DTOs, sealed interfaces for closed hierarchies, exhaustive switch for65 dispatch.665. **Do not force a modern idiom where it changes the pattern's intent.** A record cannot be67 a mutable aggregate root; a sealed hierarchy is not a plugin point.686. **When a pattern looks obsolete, separate the idea from its implementation.** Most69 classical patterns have been absorbed, not refuted — the idea still explains the70 framework's behaviour.7172## Decision rules7374```text75The framework provides the pattern completely76 → configure it first; add a wrapper only for a demonstrated semantic boundary.7778The framework provides the mechanism, you own the decision79 → make the decision explicitly and write it down. This is where80 the pattern knowledge actually pays.8182The framework provides something similar with different guarantees83 → read the guarantee. Context-local identity is not cross-context caching;84 context lifetime is integration-dependent; bulk updates need explicit85 version participation.8687The framework does not provide it88 → design it, using the pattern as the starting point rather89 than the answer (pattern-selection-and-composition).9091A pattern's classical implementation conflicts with a modern idiom92 → keep the intent, change the implementation. Immutability,93 records and sealed types usually express the intent better.9495A pattern appears obsolete96 → check whether it was absorbed rather than refuted. Table97 Module's idea survives as set-based SQL; a Row Data Gateway98 must still own row persistence, not just carry projection data.99```100101## Rules102103- **Do not mechanically wrap a framework abstraction.** A `CacheService` over the104 caching abstraction, a `TransactionService` over `@Transactional`, an `HttpService` over105 `RestClient` — each adds a name, removes features, and will not survive replacing the106 framework anyway unless it narrows capability, owns domain semantics, translates failures or107 provides a genuine replacement/test seam (`enterprise-architecture-smells`).108- **Spring Data does not decide your aggregate boundary.** It generates an implementation.109 Which aggregates exist, what the repository's surface is, and whether reads go through it110 remain design decisions and are the whole content of the pattern111 (`repository-pattern`).112- A transaction-scoped persistence context is the common unit-of-work lifetime. Extended contexts113 and Open Session In View can outlive one service transaction; evaluate their explicit consistency,114 query and connection behavior rather than calling every longer scope inherently worse115 (`orm-behavioral-patterns`).116- **The first-level cache provides context-local identity.** It lasts with the persistence117 context, which need not end at transaction completion. It does not provide freshness or118 thread safety. Cross-context caching requires an explicit cache/provider and invalidation119 contract (`caching-strategies`).120- `@Version` implements managed-entity conflict detection. The client's original version,121 conflict presentation, valid retry and bulk SQL participation remain application concerns122 (`offline-concurrency-control`).123- **JPA does not require public JavaBean setters.** Portable entities need a public/protected124 no-arg constructor and a valid field or property access strategy. Field access supports125 mutation through domain methods; persistent fields must not be final (`domain-logic-organization`).126- Records are often effective for immutable values, DTOs, commands and events. They are not JPA127 entities and do not fit aggregates that require in-place mutation/proxying, but aggregate state is128 not mutable “by definition”; immutable replacement/event-sourced models exist.129- Sealed interfaces plus exhaustive `switch` give a closed hierarchy with compile-checked130 handling. That is better than a Special Case subclass where callers must distinguish, and131 worse where they must not (`enterprise-base-patterns`).132- Virtual threads make thread-per-task blocking designs competitive for I/O-heavy Java services;133 they do not make them a universal default. Pinning, native calls, downstream capacity, memory and134 framework support still decide. They change none of135 these patterns; what they change is the sizing arithmetic around them136 (`thread-sizing-and-virtual-threads`). A pattern that was chosen to avoid blocking a137 platform thread may be worth revisiting; one chosen for a domain reason is not.138- **A pattern absorbed by a framework is still worth understanding.** The framework's139 surprising behaviours are the pattern's classical consequences, and someone who knows the140 pattern predicts them instead of debugging them.141142For the proposed implementation, return the framework mechanism and its verified scope,143the application responsibility it leaves open, and a targeted test of the relevant gap144(for example rollback, context lifetime, stale-client writes or cache interception).145When configuration evidence is missing, state the assumption and how to verify it; do not146present the feature as an established guarantee. Keep the response proportional to the task.147148## References149150- [What the framework already provides](references/framework-equivalents.md) — pattern by151 pattern: what Spring and JPA implement, what they guarantee, the gap between the classical152 pattern and the framework's version, and the wrapper to avoid in each case. Read before153 implementing any classical pattern in a Spring stack.154- [Modern Java expression](references/modern-java-idioms.md) — records, sealed types,155 exhaustive switch, immutability and virtual threads applied to the enterprise patterns:156 where they express the intent better, where they conflict with it, and the persistence157 constraints that decide which. Read when writing a pattern in current Java, or when158 modernising an old implementation.