Data Source Patterns
Purpose
Pick the data-access pattern that matches the logic it will serve, and know precisely what
each one couples to what. Inspect both who owns database access and how independently
the object model must evolve from the schema. Shape alone does not classify a pattern.
The failure this prevents is choosing by default — JPA entities for everything because the
starter is on the classpath, or a hand-rolled DAO layer because the previous project had
one — and then fighting the consequences for years in code that looks like a mapping
problem but is a pattern-selection problem.
The four patterns
Table Data Gateway one gateway for a table or view; its SQL lives
there; methods take and return primitives or record
sets. No domain objects, no per-row identity.
Row Data Gateway one object per row; it holds the row's data and
knows how to load and save itself. No business
logic — that distinction is the whole point.
Active Record Row Data Gateway plus the business logic for that
row. Object shape follows table shape; persistence
is a method on the object.
Data Mapper a separate mapper moves data between objects and
tables. The objects know nothing about persistence,
so the two shapes may diverge freely.
An ORM like JPA/Hibernate is a Data Mapper implementation with an identity map and a unit
of work attached. Spring Data JDBC is closer to a Data Mapper with a simpler contract;
JdbcClient is a JDBC facade, not a pattern choice: its caller can implement a table
gateway, aggregate repository or mapper. Classify responsibilities, not class names.
Workflow
Examples are partial Java 17 snippets; JdbcClient requires Spring Framework 6.1+.
Inspect the project's compiler/toolchain, resolved Spring/JPA versions, database dialect,
schema constraints and transaction configuration before adapting them. Use existing JDBC
APIs on older stacks; adopting this skill does not authorize a dependency/runtime upgrade.
- Start from the logic organisation, which is the prior decision
(
domain-logic-organization). Transaction scripts pair naturally with gateways; a
domain model needs a mapper — or Active Record if the model happens to mirror the tables.
- Measure the shape gap. Compare the object model you want with the schema you have:
count the places where one concept spans several tables, one table serves several
concepts, or the schema is owned elsewhere. A rich model with a wide gap favors a mapper;
a reporting projection against that same schema may only need a gateway.
- Establish who owns the schema. If it is yours and it can follow the model, Active
Record may be economical. If it is externally owned, inspect actual constraints and change
history: isolate translation when domain behavior needs independence, without inventing a domain model for reports.
- Check the work shape. Set-based and reporting work belongs in a gateway with SQL,
whatever the write side uses. Mixing is normal and correct.
- Decide per module. A pricing engine with a mapper and an admin CRUD area with Active
Record in one application is a reasonable design, provided the boundary between them is
explicit.
- Write down what the choice couples, so the next person understands the cost of
changing it.
Decision rules
Transaction scripts, set-based work, reporting, imports
→ Consider Table Data Gateway for explicit set-oriented access.
Keep business-policy ownership outside it; technical checks are allowed.
Simple CRUD, schema mirrors the model, you own the schema, thin rules
→ Active Record is a candidate if persistence methods on the
object fit the existing stack; a simple mapper is also reasonable.
Rich interacting business rules; model and schema will diverge
→ Data Mapper. This is what the pattern exists for and the cost
is the mapping layer.
Schema owned elsewhere, or legacy, or shaped for reporting
→ Mapper plus translation when a domain model needs isolation;
a read/reporting gateway may suffice. Do not let the foreign schema
become the model (legacy-enterprise-modernization).
Domain model already chosen, and the schema currently matches it
→ JPA entities are a Data Mapper whose mapping is declarative.
Acceptable and common; the risk is that the annotations start
shaping the model (orm-structural-mapping).
Rules need testing without a database
→ First isolate rule execution from I/O. Both a JPA entity and an
Active Record can have pure rule methods. Add a separate domain
model only when mapping constraints justify it.
Read path of an application whose write path uses a mapper
→ Consider a gateway/projection when loading the write model adds
measured cost; ordinary aggregate reads remain legitimate.
Rules
- Active Record is not a beginner's pattern. A close object/row correspondence and
simple rules can make combined access and behavior economical. Revisit when observed
mapping or testing constraints obstruct the model, rather than on a pattern label alone.
- Active Record combines persistence and domain behavior in one object, often keeping a
close row shape. Mapping features can absorb column renames or value types; inspect actual
change propagation rather than asserting every domain change needs a migration.
- Data Mapper's real cost is the mapping layer: code to write, code to test, and a place
for bugs that neither the model nor the schema exhibits alone. It is worth paying when
independence or mapping complexity warrants it, even when current shapes happen to match.
- A JPA entity without persistence methods remains part of a Data Mapper-style unit of work, even
when persistence constraints distort its design; call that persistence leakage, not Active
Record. If the model has an
Integer where the domain means an enum, a flattened
address because @Embeddable was awkward, or a bidirectional association that exists
only for a mapping, the schema is already shaping the model — decide deliberately whether
that is acceptable here.
- Do not build a DAO layer that wraps a repository that wraps the ORM. Each layer must add
behaviour — a translation, a policy, an aggregate boundary — or it is indirection
(
enterprise-architecture-smells).
- Table Data Gateway pairs with transaction scripts. Resource handling, row-count checks,
SQL predicates and optimistic concurrency checks are legitimate conditionals. Hidden
business-policy decisions migrating into the gateway are the concern, and
it is hard to find later because nobody looks for rules in a data-access class.
- Row Data Gateway is rarely chosen deliberately today, but it names a useful boundary: a
row object with no business logic. When a "domain object" has only accessors and
persistence, that is what it is, and calling it that clarifies the design
(
domain-logic-organization).
- Different patterns may coexist. Multiple mechanisms can write one table during migrations or for
deliberately partitioned operations, but they must share invariant, versioning and transaction
rules with explicit write authority. Uncoordinated writers are the defect
(
offline-concurrency-control).
- SQL can avoid object loading for reporting and bulk changes; verify plans, rows touched,
round trips and application latency. Set-based writes must preserve invariants, versioning
and transaction semantics; ORM callbacks and managed state may be bypassed.
Deliver a pattern choice grounded in one representative read/write path, ownership and
coupling costs, plus a validation case and revisit trigger. If schema or lifecycle evidence
is missing, state the conditional choice instead of prescribing a migration.
References
- Gateways, Row Data Gateway and Active Record
— each pattern implemented in modern Java, what belongs in each and what must not, where
the gateway degrades, Active Record in a Spring stack and its honest limits, and the
signals that a pattern has been outgrown. Read when implementing or reviewing one of
them.
- Active Record versus Data Mapper — the
comparison dimension by dimension with the same domain implemented both ways, what each
actually couples, testability and query control, the JPA middle ground and its risks, and
the migration path when the choice must change. Read when the decision is genuinely open
or is being re-opened.
1---2name: data-source-patterns3description: Choosing how code reaches the database — Table Data Gateway, Row Data Gateway, Active Record or Data Mapper — from the shape of the domain logic rather than from framework habit, and knowing what each one couples together. Use when a new module's persistence approach is being chosen, when entities carry both business rules and save() methods, when JPA is being applied to a schema that fights it, when SQL is scattered through service classes, when a "DAO" layer duplicates what the ORM already provides, when the domain model's shape is visibly dictated by the tables, when bulk or reporting work is being forced through an ORM, or when a team is arguing Active Record versus Data Mapper in the abstract. Does not cover where business logic lives (domain-logic-organization), the ORM's runtime behaviour — unit of work, identity map, lazy load (orm-behavioral-patterns), the column-level mapping decisions (orm-structural-mapping), or the collection-shaped abstraction over aggregates (repository-pattern).4---56# Data Source Patterns78## Purpose910Pick the data-access pattern that matches the logic it will serve, and know precisely what11each one couples to what. Inspect both **who owns database access** and how independently12the object model must evolve from the schema. Shape alone does not classify a pattern.1314The failure this prevents is choosing by default — JPA entities for everything because the15starter is on the classpath, or a hand-rolled DAO layer because the previous project had16one — and then fighting the consequences for years in code that looks like a mapping17problem but is a pattern-selection problem.1819## The four patterns2021```text22Table Data Gateway one gateway for a table or view; its SQL lives23 there; methods take and return primitives or record24 sets. No domain objects, no per-row identity.2526Row Data Gateway one object per row; it holds the row's data and27 knows how to load and save itself. No business28 logic — that distinction is the whole point.2930Active Record Row Data Gateway plus the business logic for that31 row. Object shape follows table shape; persistence32 is a method on the object.3334Data Mapper a separate mapper moves data between objects and35 tables. The objects know nothing about persistence,36 so the two shapes may diverge freely.37```3839An ORM like JPA/Hibernate is a Data Mapper implementation with an identity map and a unit40of work attached. Spring Data JDBC is closer to a Data Mapper with a simpler contract;41`JdbcClient` is a JDBC facade, not a pattern choice: its caller can implement a table42gateway, aggregate repository or mapper. Classify responsibilities, not class names.4344## Workflow4546Examples are partial Java 17 snippets; `JdbcClient` requires Spring Framework 6.1+.47Inspect the project's compiler/toolchain, resolved Spring/JPA versions, database dialect,48schema constraints and transaction configuration before adapting them. Use existing JDBC49APIs on older stacks; adopting this skill does not authorize a dependency/runtime upgrade.50511. **Start from the logic organisation**, which is the prior decision52 (`domain-logic-organization`). Transaction scripts pair naturally with gateways; a53 domain model needs a mapper — or Active Record if the model happens to mirror the tables.542. **Measure the shape gap.** Compare the object model you want with the schema you have:55 count the places where one concept spans several tables, one table serves several56 concepts, or the schema is owned elsewhere. A rich model with a wide gap favors a mapper;57 a reporting projection against that same schema may only need a gateway.583. **Establish who owns the schema.** If it is yours and it can follow the model, Active59 Record may be economical. If it is externally owned, inspect actual constraints and change60 history: isolate translation when domain behavior needs independence, without inventing a domain model for reports.614. **Check the work shape.** Set-based and reporting work belongs in a gateway with SQL,62 whatever the write side uses. Mixing is normal and correct.635. **Decide per module.** A pricing engine with a mapper and an admin CRUD area with Active64 Record in one application is a reasonable design, provided the boundary between them is65 explicit.666. **Write down what the choice couples**, so the next person understands the cost of67 changing it.6869## Decision rules7071```text72Transaction scripts, set-based work, reporting, imports73 → Consider Table Data Gateway for explicit set-oriented access.74 Keep business-policy ownership outside it; technical checks are allowed.7576Simple CRUD, schema mirrors the model, you own the schema, thin rules77 → Active Record is a candidate if persistence methods on the78 object fit the existing stack; a simple mapper is also reasonable.7980Rich interacting business rules; model and schema will diverge81 → Data Mapper. This is what the pattern exists for and the cost82 is the mapping layer.8384Schema owned elsewhere, or legacy, or shaped for reporting85 → Mapper plus translation when a domain model needs isolation;86 a read/reporting gateway may suffice. Do not let the foreign schema87 become the model (legacy-enterprise-modernization).8889Domain model already chosen, and the schema currently matches it90 → JPA entities are a Data Mapper whose mapping is declarative.91 Acceptable and common; the risk is that the annotations start92 shaping the model (orm-structural-mapping).9394Rules need testing without a database95 → First isolate rule execution from I/O. Both a JPA entity and an96 Active Record can have pure rule methods. Add a separate domain97 model only when mapping constraints justify it.9899Read path of an application whose write path uses a mapper100 → Consider a gateway/projection when loading the write model adds101 measured cost; ordinary aggregate reads remain legitimate.102```103104## Rules105106- **Active Record is not a beginner's pattern.** A close object/row correspondence and107 simple rules can make combined access and behavior economical. Revisit when observed108 mapping or testing constraints obstruct the model, rather than on a pattern label alone.109- Active Record combines persistence and domain behavior in one object, often keeping a110 close row shape. Mapping features can absorb column renames or value types; inspect actual111 change propagation rather than asserting every domain change needs a migration.112- Data Mapper's real cost is the mapping layer: code to write, code to test, and a place113 for bugs that neither the model nor the schema exhibits alone. It is worth paying when114 independence or mapping complexity warrants it, even when current shapes happen to match.115- A JPA entity without persistence methods remains part of a Data Mapper-style unit of work, even116 when persistence constraints distort its design; call that **persistence leakage**, not Active117 Record. If the model has an `Integer` where the domain means an enum, a flattened118 address because `@Embeddable` was awkward, or a bidirectional association that exists119 only for a mapping, the schema is already shaping the model — decide deliberately whether120 that is acceptable here.121- Do not build a DAO layer that wraps a repository that wraps the ORM. Each layer must add122 behaviour — a translation, a policy, an aggregate boundary — or it is indirection123 (`enterprise-architecture-smells`).124- Table Data Gateway pairs with transaction scripts. Resource handling, row-count checks,125 SQL predicates and optimistic concurrency checks are legitimate conditionals. Hidden126 business-policy decisions migrating into the gateway are the concern, and127 it is hard to find later because nobody looks for rules in a data-access class.128- Row Data Gateway is rarely chosen deliberately today, but it names a useful boundary: a129 row object with no business logic. When a "domain object" has only accessors and130 persistence, that is what it is, and calling it that clarifies the design131 (`domain-logic-organization`).132- Different patterns may coexist. Multiple mechanisms can write one table during migrations or for133 deliberately partitioned operations, but they must share invariant, versioning and transaction134 rules with explicit write authority. Uncoordinated writers are the defect135 (`offline-concurrency-control`).136- SQL can avoid object loading for reporting and bulk changes; verify plans, rows touched,137 round trips and application latency. Set-based writes must preserve invariants, versioning138 and transaction semantics; ORM callbacks and managed state may be bypassed.139140Deliver a pattern choice grounded in one representative read/write path, ownership and141coupling costs, plus a validation case and revisit trigger. If schema or lifecycle evidence142is missing, state the conditional choice instead of prescribing a migration.143144## References145146- [Gateways, Row Data Gateway and Active Record](references/gateways-and-active-record.md)147 — each pattern implemented in modern Java, what belongs in each and what must not, where148 the gateway degrades, Active Record in a Spring stack and its honest limits, and the149 signals that a pattern has been outgrown. Read when implementing or reviewing one of150 them.151- [Active Record versus Data Mapper](references/active-record-vs-data-mapper.md) — the152 comparison dimension by dimension with the same domain implemented both ways, what each153 actually couples, testability and query control, the JPA middle ground and its risks, and154 the migration path when the choice must change. Read when the decision is genuinely open155 or is being re-opened.