Repository Pattern in Clean Architecture vs Spring Data JPA
Fast recall
Quick revision
Core answer
JpaRepository is infrastructure, not a domain repository; in Clean Architecture you wrap it inside an adapter that implements your port. This separation lets you switch persistence technologies without touching the domain.Repository as a domain abstraction (port) vs an infrastructure interface (Spring Data JPA), why the inner layer owns the contract, and the mapping between domain entities and JPA entities.
Treating Spring Data JPA's interface as the domain repository, leaking JPA into the use case layer.
Discuss aggregate-level repositories (vs CRUD per table), specification pattern, query methods on repositories vs separate query handlers (CQRS), and how to handle complex queries without leaking infrastructure.
Interview-ready guide
Interview answer
What a repository is in Clean Architecture
Collection-like abstraction for retrieving and persisting domain objects (typically aggregates). Exposes operations the domain needs: save(order), findById(id), findByIdempotencyKey(key), delete(order).
Does not expose: JPA-specific things (@Query, criteria API), infrastructure details (connection pool, paging APIs), generic CRUD shaped by storage rather than domain.
Repository interface lives in the use case (application) layer. Implementation lives in persistence adapter. Domain depends on interface, not implementation.
Spring Data JPA — what it actually is
JpaRepository<OrderEntity, String> is a Spring Data infrastructure interface. Knows about JPA, returns JPA entities, lifecycle tied to persistence context, methods like findByCustomerIdAndStatus translated into JPQL.
Great productivity tool for the persistence adapter — not the domain repository. Treating it as the domain repository leaks JPA into the use case. JPA entities with lazy proxies become domain objects; you've coupled business logic to persistence framework.
In Clean Architecture, JpaRepository is hidden behind the adapter — useful tool to implement the port, never the port itself.
Repository port + adapter — the right setup
Application layer: OrderRepository interface with save, findById, findByIdempotencyKey, findActiveByCustomerId. Returns domain Order.
Adapter layer: OrderJpaEntity (@Entity, @Table, @OneToMany on items). OrderSpringRepo extends JpaRepository<OrderJpaEntity, String> with JPA-specific finders. JpaOrderRepository implements OrderRepository, uses OrderSpringRepo + OrderMapper to convert.
Use cases depend on OrderRepository. JPA invisible above the adapter.
Aggregate-per-repository, not table-per-repository
Spring Data tutorials suggest one repository per @Entity. In Clean Architecture/DDD, repository operates on an aggregate — cluster of objects treated as one consistency boundary.
Anti-pattern: OrderRepository + OrderItemRepository + OrderShipmentRepository + OrderPaymentRepository. Four for one logical aggregate.
Right: just OrderRepository, saves Order with items, shipment, payment as one unit. Persistence adapter maps to multiple JPA entities behind the scenes.
Aggregate root: only entity a repository persists directly. Other objects reachable through it (order.items, order.shipment).
Complex queries — two approaches
Approach 1: add finder methods to the repository. findActiveByCustomerId, findOverdueByDueDate. Fine for a handful, doesn't scale to dozens of report-style queries.
Approach 2: CQRS-style separate query interfaces. OrderQueryService.findSummary(orderId), search(criteria, page), monthlyStats(month). Returns DTOs, not domain objects. Implementation can use SQL views, projections, separate read model. Keeps write-side OrderRepository focused on the aggregate's lifecycle.
For read-heavy systems with complex query needs, separating commands from queries (CQRS lite) is the practical choice. Don't try to express every query as a domain repository method.
When to use Spring Data directly
Directly when: not a domain aggregate (Country, Currency lookup table), read-only DTO projections for query screens, internal service that doesn't aspire to Clean Architecture rigor.
Wrap behind a port when: domain aggregate/entity, use case layer needs to be framework-free, might swap persistence technology, pure-unit tests on use cases.
Clean Architecture isn't religious — it's cost-benefit. Wrapping every JPA call has a cost. Pay it for aggregates; skip it for trivial lookups.
Common mistakes
- Use case importing JpaRepository — JPA leaks upward
- Repository per JPA entity — should be per aggregate
- Returning JPA entities from repository port — use case still coupled
- CRUD-style repository with findAll returning 1M rows
- Business logic in repository — repositories persist, don't decide
- No mapping between domain and JPA entities — leaks JPA concerns
- Specification/Criteria API in use case — adapter handles the query
Recommendation
Define repositories as ports owned by the use case layer, written in domain vocabulary, returning domain objects. Implement them in the persistence adapter using Spring Data JPA as the underlying tool — wrap, don't expose. One repository per aggregate (not per table). For complex query needs, add CQRS-style query services that return DTOs and can hit projections or SQL views directly. Don't over-apply: trivial lookup tables can use Spring Data directly without a port. The discipline pays off for aggregates with real business behavior; for read-only DTOs, the wrapping is overhead.
Repository Pattern in Clean Architecture vs Spring Data JPA
Direct Answer
In Clean Architecture, a Repository is a port (interface) defined by the use case layer, written in the domain's vocabulary, returning domain objects. The implementation lives in the persistence adapter — it can use Spring Data JPA, JDBC, MongoDB, or in-memory — and translates between domain entities and the underlying storage. Spring Data JPA's JpaRepository is infrastructure, not a domain repository; in Clean Architecture you wrap it inside an adapter that implements your port. This separation lets you switch persistence technologies without touching the domain.
Common Mistakes & Pitfalls
Treating Spring Data JPA's interface as the domain repository, leaking JPA into the use case layer.
Check your understanding
Answer these without looking back at the article.
- Walk through saving an order aggregate that has items, a shipment, and a payment.
- How do you handle complex search queries without polluting the use case with infrastructure?
- When is it OK to use Spring Data JPA directly without a port wrapper?
- What's the relationship between a DDD repository and a Clean Architecture repository?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.