Repository Pattern
Purpose
Provide a collection-like boundary between domain objects and data mapping. Fowler's
Repository includes queries over domain objects; it does not universally require one DDD
aggregate root or prohibit read projections. This skill emphasizes DDD write repositories:
independent child writes must not bypass the root's invariants. Separate read gateways when
their query shape, cost or ownership warrants it, and remove abstractions only when their
contract and dependency boundary add no value.
The two failures are the layered nothing (Service → Repository → BaseRepository →
GenericDao → ORM, with no layer adding behaviour) and the leaky everything (a repository
leaking uncontrolled managed-state lifetimes or exposing Pageable, Specification and EntityManager, so the
persistence technology is present everywhere it was supposed to be absent).
What a repository is, and is not
IS: a collection-like interface over domain objects
typically one aggregate root for DDD write access
add / remove / find by identity / find by domain criteria
expressed in domain types
for a domain-facing port: domain/application owned, adapter implemented
a participant in the use case's transaction, possibly with local defaults
IS NOT: a per-table data access object
a home for business operations
a requirement that every reporting/screen query hydrate an aggregate
a portability layer over the database
Workflow
Inspect Java/toolchain, Spring Data/JPA/provider versions, transaction/proxy configuration
and the existing mapping model before recommending APIs. The examples are partial Java 17
source shapes with application types omitted, not a complete Spring project. Spring metadata
guidance was checked against Spring Data JPA 4.1.1 documentation. Adapt to the
project baseline without adding libraries or upgrading Java for the example. Return the
chosen write/read contract, object lifetime, transaction boundary and checks needed to prove it.
- Identify aggregate consistency boundaries. A repository is normally per aggregate root.
Dedicated child/query gateways may exist for bulk operations or read models without granting
independent domain mutation (
domain-logic-organization).
- Write the interface in the domain's language, in domain types:
Orders.byId,
Orders.overdueFor(customer), Orders.save. Not OrderJpaRepository with
findAllByStatusIn.
- Choose the read path deliberately. Screens and reports can use projections or query
gateways when aggregate loading is unsuitable. Spring Data projection methods or a small
shared interface can also be appropriate; separation is a design choice, not the definition
of Repository (
query-objects-and-specifications).
- Decide what crosses the boundary. Domain aggregates out; identifiers and domain
values in. Choose managed aggregates confined to the transactional use case or mapped
independent domain objects; controllers/serialization should receive an explicit read DTO.
- Inspect business verbs for hidden policy.
cancelExpired() must not hide eligibility,
transitions or event rules in persistence code. A domain-defined bulk operation may be
delegated only when its invariant, concurrency and effect semantics are preserved.
- Ask whether the boundary earns its cost in this module. For a CRUD module with no
aggregate and no invariant, direct Spring Data is a candidate if no distinct boundary
contract is needed.
Decision rules
An aggregate root with invariants and an explicit persistence contract
→ one repository, domain-typed interface, implementation in the
adapter. This is the pattern doing its job.
A child entity inside an aggregate
→ mutations go through the root's consistency rules. Internal persistence
gateways and read projections are valid; independently callable writes
bypassing the root require redesign (orm-structural-mapping).
Reads for a screen, a report, an export
→ consider a query object/projection without aggregate hydration;
the interface name alone does not decide correctness.
A CRUD module: no invariants, entity ≈ table, no aggregate
→ direct Spring Data (or a gateway) is a candidate when no distinct
domain/application port, capability limit or policy boundary is needed.
A domain-owned interface with a single adapter implementation whose
methods are identical to Spring Data's
→ the wrapper is indirection unless it is doing something: type
translation, hiding framework types, or narrowing the surface.
Narrowing IS a real justification; identical signatures are not.
A "generic repository" with type parameters serving every entity
→ reject a mandatory broad CRUD surface for unrelated aggregates.
A narrow internal base for shared mechanics can coexist with
domain-specific interfaces; judge the exposed capabilities.
Bulk or set-based work over the aggregate's table
→ a gateway with SQL, named as such, with its interaction with
versioning and the persistence context handled explicitly
(offline-concurrency-control).
Rules
- Prefer one domain repository per aggregate root. Per-table gateways are valid infrastructure for
set-based/query work; the defect is exposing independent child mutation that bypasses aggregate
invariants while calling it a domain repository.
- For a domain-facing port, the interface belongs to the domain/application abstraction and
the implementation to the adapter. That is
the inversion that makes the domain testable and the persistence replaceable, and it is
a strong structural reason to hand-write the interface, alongside narrowing capabilities,
domain-specific error semantics, testing seams and multiple adapters (
layering-and-boundaries).
- Keep business policy in the domain/use case.
orders.cancelExpired() deserves inspection
for hidden eligibility, transition and event rules; the verb alone is not proof. A bulk
adapter may execute a domain-defined operation only with equivalent invariant/concurrency
and effect semantics explicitly established.
- Do not leak persistence types through the interface.
Pageable, Specification,
Sort, EntityManager, Page in a domain-owned interface mean the domain now depends on
persistence frameworks, undermining the intended dependency boundary.
- Managed domain entities within a transactional use case are a valid JPA choice. Document
dirty checking versus explicit save, detached results and lazy-access boundaries; transaction
completion alone does not always end an extended persistence context. Map outward-facing
DTOs before required lazy state becomes unavailable (
orm-behavioral-patterns).
- Reads and writes have different requirements and may legitimately use different paths.
Measure query counts, fetched rows/bytes and hydration before attributing slow screens to
the repository structure (
architecture-and-performance).
existsBy(...) followed by save(...) is a race, not a check. Uniqueness is enforced by
a constraint; the repository call only produces a better error message
(enterprise-transactions).
- Repositories may provide local transaction defaults, but an outer application transaction usually
joins/overrides them under
REQUIRED. Without an outer boundary, two sequential repository calls
can commit independently. Test the actual propagation and proxy path (service-layer-design).
- Spring Data supplies common repository mechanics, not the whole design.
What remains a decision is the interface's shape, its ownership, and whether an aggregate
boundary exists at all.
extends JpaRepository<Order, Long> publishes a broad surface
including deleteAll() — that is a surface decision, not a default.
- For Spring Data query/CRUD methods, configure
@Lock/@QueryHints where its metadata
machinery reads them. A custom fragment or direct EntityManager implementation must apply
lock modes/hints explicitly or through a verified integration; an annotation alone does
not change arbitrary Java code. Verify actual SQL and transaction scope.
- Remove forwarding layers only after checking capability narrowing, dependency ownership,
transaction/authorization policy, error translation and test seams. An intentionally stable
domain port can be valuable with one adapter and forwarding bodies (
enterprise-architecture-smells).
References
- Repository boundaries — the domain-owned interface
with its adapter implementation in Java, what the aggregate boundary means for the
methods, reconstitution and detachment, read models beside the repository, and the
narrowing that justifies a hand-written interface over Spring Data. Read when designing or
reviewing a repository.
- Repository misuse — the layered nothing, the generic
repository, business verbs, child-entity repositories, leaked framework and managed types,
and the check-then-act race; each with detection and the concrete fix, plus when a thin
wrapper is nevertheless correct. Read when auditing an existing data layer.
1---2name: repository-pattern3description: The repository as a collection-like boundary over domain objects, with aggregate-root write boundaries in DDD: what belongs behind it, where queries and read models fit, and when a redundant CRUD wrapper can be removed without losing a useful contract. Use when a repository is being added for a child entity, when a generic or base repository is proposed, when repository methods carry business verbs (cancelExpired, activateEligible), when a managed entity escapes through the repository interface, when reads and writes both go through the same interface and reads are slow, when a repository interface wraps a Spring Data interface that wraps the ORM, or when someone argues that Spring Data repositories make the pattern unnecessary. Does not cover query composition (query-objects-and-specifications), ORM runtime behaviour (orm-behavioral-patterns), which data-access pattern underlies it (data-source-patterns), or aggregate design itself (domain-logic-organization).4---56# Repository Pattern78## Purpose910Provide a collection-like boundary between domain objects and data mapping. Fowler's11Repository includes queries over domain objects; it does not universally require one DDD12aggregate root or prohibit read projections. This skill emphasizes DDD write repositories:13independent child writes must not bypass the root's invariants. Separate read gateways when14their query shape, cost or ownership warrants it, and remove abstractions only when their15contract and dependency boundary add no value.1617The two failures are the layered nothing (`Service` → `Repository` → `BaseRepository` →18`GenericDao` → ORM, with no layer adding behaviour) and the leaky everything (a repository19leaking uncontrolled managed-state lifetimes or exposing `Pageable`, `Specification` and `EntityManager`, so the20persistence technology is present everywhere it was supposed to be absent).2122## What a repository is, and is not2324```text25IS: a collection-like interface over domain objects26 typically one aggregate root for DDD write access27 add / remove / find by identity / find by domain criteria28 expressed in domain types29 for a domain-facing port: domain/application owned, adapter implemented30 a participant in the use case's transaction, possibly with local defaults3132IS NOT: a per-table data access object33 a home for business operations34 a requirement that every reporting/screen query hydrate an aggregate35 a portability layer over the database36```3738## Workflow3940Inspect Java/toolchain, Spring Data/JPA/provider versions, transaction/proxy configuration41and the existing mapping model before recommending APIs. The examples are partial Java 1742source shapes with application types omitted, not a complete Spring project. Spring metadata43guidance was checked against Spring Data JPA 4.1.1 documentation. Adapt to the44project baseline without adding libraries or upgrading Java for the example. Return the45chosen write/read contract, object lifetime, transaction boundary and checks needed to prove it.46471. **Identify aggregate consistency boundaries.** A repository is normally per aggregate root.48 Dedicated child/query gateways may exist for bulk operations or read models without granting49 independent domain mutation (`domain-logic-organization`).502. **Write the interface in the domain's language**, in domain types: `Orders.byId`,51 `Orders.overdueFor(customer)`, `Orders.save`. Not `OrderJpaRepository` with52 `findAllByStatusIn`.533. **Choose the read path deliberately.** Screens and reports can use projections or query54 gateways when aggregate loading is unsuitable. Spring Data projection methods or a small55 shared interface can also be appropriate; separation is a design choice, not the definition56 of Repository (`query-objects-and-specifications`).574. **Decide what crosses the boundary.** Domain aggregates out; identifiers and domain58 values in. Choose managed aggregates confined to the transactional use case or mapped59 independent domain objects; controllers/serialization should receive an explicit read DTO.605. **Inspect business verbs for hidden policy.** `cancelExpired()` must not hide eligibility,61 transitions or event rules in persistence code. A domain-defined bulk operation may be62 delegated only when its invariant, concurrency and effect semantics are preserved.636. **Ask whether the boundary earns its cost in this module.** For a CRUD module with no64 aggregate and no invariant, direct Spring Data is a candidate if no distinct boundary65 contract is needed.6667## Decision rules6869```text70An aggregate root with invariants and an explicit persistence contract71 → one repository, domain-typed interface, implementation in the72 adapter. This is the pattern doing its job.7374A child entity inside an aggregate75 → mutations go through the root's consistency rules. Internal persistence76 gateways and read projections are valid; independently callable writes77 bypassing the root require redesign (orm-structural-mapping).7879Reads for a screen, a report, an export80 → consider a query object/projection without aggregate hydration;81 the interface name alone does not decide correctness.8283A CRUD module: no invariants, entity ≈ table, no aggregate84 → direct Spring Data (or a gateway) is a candidate when no distinct85 domain/application port, capability limit or policy boundary is needed.8687A domain-owned interface with a single adapter implementation whose88methods are identical to Spring Data's89 → the wrapper is indirection unless it is doing something: type90 translation, hiding framework types, or narrowing the surface.91 Narrowing IS a real justification; identical signatures are not.9293A "generic repository" with type parameters serving every entity94 → reject a mandatory broad CRUD surface for unrelated aggregates.95 A narrow internal base for shared mechanics can coexist with96 domain-specific interfaces; judge the exposed capabilities.9798Bulk or set-based work over the aggregate's table99 → a gateway with SQL, named as such, with its interaction with100 versioning and the persistence context handled explicitly101 (offline-concurrency-control).102```103104## Rules105106- Prefer one domain repository per aggregate root. Per-table gateways are valid infrastructure for107 set-based/query work; the defect is exposing independent child mutation that bypasses aggregate108 invariants while calling it a domain repository.109- For a domain-facing port, the interface belongs to the domain/application abstraction and110 the implementation to the adapter. That is111 the inversion that makes the domain testable and the persistence replaceable, and it is112 a strong structural reason to hand-write the interface, alongside narrowing capabilities,113 domain-specific error semantics, testing seams and multiple adapters (`layering-and-boundaries`).114- Keep business policy in the domain/use case. `orders.cancelExpired()` deserves inspection115 for hidden eligibility, transition and event rules; the verb alone is not proof. A bulk116 adapter may execute a domain-defined operation only with equivalent invariant/concurrency117 and effect semantics explicitly established.118- **Do not leak persistence types through the interface.** `Pageable`, `Specification`,119 `Sort`, `EntityManager`, `Page` in a domain-owned interface mean the domain now depends on120 persistence frameworks, undermining the intended dependency boundary.121- Managed domain entities within a transactional use case are a valid JPA choice. Document122 dirty checking versus explicit save, detached results and lazy-access boundaries; transaction123 completion alone does not always end an extended persistence context. Map outward-facing124 DTOs before required lazy state becomes unavailable (`orm-behavioral-patterns`).125- Reads and writes have different requirements and may legitimately use different paths.126 Measure query counts, fetched rows/bytes and hydration before attributing slow screens to127 the repository structure (`architecture-and-performance`).128- `existsBy(...)` followed by `save(...)` is a race, not a check. Uniqueness is enforced by129 a constraint; the repository call only produces a better error message130 (`enterprise-transactions`).131- Repositories may provide local transaction defaults, but an outer application transaction usually132 joins/overrides them under `REQUIRED`. Without an outer boundary, two sequential repository calls133 can commit independently. Test the actual propagation and proxy path (`service-layer-design`).134- **Spring Data supplies common repository mechanics, not the whole design.**135 What remains a decision is the interface's shape, its ownership, and whether an aggregate136 boundary exists at all. `extends JpaRepository<Order, Long>` publishes a broad surface137 including `deleteAll()` — that is a surface decision, not a default.138- For Spring Data query/CRUD methods, configure `@Lock`/`@QueryHints` where its metadata139 machinery reads them. A custom fragment or direct EntityManager implementation must apply140 lock modes/hints explicitly or through a verified integration; an annotation alone does141 not change arbitrary Java code. Verify actual SQL and transaction scope.142- Remove forwarding layers only after checking capability narrowing, dependency ownership,143 transaction/authorization policy, error translation and test seams. An intentionally stable144 domain port can be valuable with one adapter and forwarding bodies (`enterprise-architecture-smells`).145146## References147148- [Repository boundaries](references/repository-boundaries.md) — the domain-owned interface149 with its adapter implementation in Java, what the aggregate boundary means for the150 methods, reconstitution and detachment, read models beside the repository, and the151 narrowing that justifies a hand-written interface over Spring Data. Read when designing or152 reviewing a repository.153- [Repository misuse](references/repository-misuse.md) — the layered nothing, the generic154 repository, business verbs, child-entity repositories, leaked framework and managed types,155 and the check-then-act race; each with detection and the concrete fix, plus when a thin156 wrapper is nevertheless correct. Read when auditing an existing data layer.