skip to content

You're reviewing a codebase where every entity has its own Repository interface exposing generic methods like findAll(), save(), delete(), plus a handful of custom finders, on top of an ORM. What are the costs of this layer versus using the ORM's session/EntityManager directly, and when would you skip introducing a Repository at all?

level: seniorimportance: should knowfreq 50%

answer

  1. ORM session is already an abstraction
  2. thin pass-through adds files, not decoupling
  3. keep it when domain logic needs fast unit tests
  4. keep it when multiple backends are a real plan
  5. Spring Data JpaRepository used directly is fine for simple CRUD

basics

~10 s

If a Repository just re-exposes the ORM's own save/find/delete methods, it's extra boilerplate with no real decoupling benefit. Skip it for small, simple apps where the ORM's API is already good enough.

solid answer

~40 s

A Repository that's a thin, one-to-one pass-through over an ORM session (save calls entityManager.persist, findById calls entityManager.find) adds a file and an indirection layer without buying real decoupling, since the ORM's session is already an abstraction and the interface just mirrors it. The cost: more files, more indirection, and a false sense that persistence technology could be swapped when nobody ever does that for a small CRUD app. Skip the pattern (or a dedicated interface per entity) when domain logic is simple CRUD, there's no plausible reason to swap persistence technology, and tests can tolerate a fast real database. It earns its keep once logic is complex enough to want isolated unit testing, or there's a genuine plan for multiple persistence backends.

go deeper

for a junior

May default to 'more abstraction is always better' and benefit from being walked through a concrete case where it isn't.

for a middle

Should recognize that a thin pass-through Repository over an ORM session adds indirection without real decoupling.

for a senior

Should give a clear decision framework (testability need, multi-backend need, domain/persistence model divergence) for when to keep versus skip the pattern, with real framework examples.

for a principal

Should be able to set a team-wide default (e.g., 'use JpaRepository directly for simple CRUD modules, introduce a hand-written interface only when a module has meaningful business logic or a concrete multi-backend requirement') and justify it against the codebase's actual complexity distribution.

## Why the layer is often pure ceremony A generic Repository per entity — `CustomerRepository`, `OrderRepository`, `ProductRepository`, each with `save`, `findById`, `findAll`, `delete`, plus a few custom finders — is the most common way teams apply this pattern in ORM-based systems, and it's also the version most prone to being pure ceremony. The reason is that a modern **ORM's session or entity-manager object** (Hibernate's `Session`, JPA's `EntityManager`, or a framework wrapper like Spring Data's generated repository) is *already* a persistence abstraction: it already hides SQL, connection management, and dialect differences behind `persist()`, `find()`, `merge()`, `remove()`. When a hand-rolled Repository interface's methods just forward one-to-one to those same operations — `save(customer)` calling `entityManager.merge(customer)` and nothing else — the Repository isn't adding a new abstraction boundary, it's **renaming an existing one**. The decoupling the pattern promises (swap the persistence technology without touching callers) was, in this scenario, already available for free by depending on the ORM's own interfaces, minus the extra file. ## The concrete costs The genuine costs of this style of over-application are concrete rather than aesthetic. - **File-count and cognitive overhead.** Every entity needs its own interface, plus (depending on the framework) an implementation class, which is pure file-count and cognitive overhead for a reader trying to trace what actually happens when `save()` is called — they now have to jump through an extra layer that does nothing but rename a method. - **A false signal to the rest of the team.** It also creates a false signal to the rest of the team: the presence of a Repository interface implies 'this could be swapped for a different implementation,' but if that swap has never happened and isn't realistically planned, the interface is a promise nobody intends to keep, and new team members can waste time maintaining an abstraction boundary that was never load-bearing. ## When the pattern still earns its keep Against that, the pattern's benefits are still real in the right context, which is why 'skip it always' would be the wrong lesson to draw. 1. **The clearest case for keeping (or introducing) the pattern is unit-testability of business logic**: if a service class enforces meaningful rules — discount eligibility, inventory reservation, workflow state transitions — and that logic needs to be exercised in fast, isolated unit tests without a real database, then depending on a Repository interface (rather than directly on `EntityManager`) lets the test substitute a simple in-memory fake, which is straightforward to write against a small, purpose-built interface but awkward or impossible against a full ORM session API. 2. **The second clear case is a genuine, planned need for multiple backends**: a product that must run against a customer's on-prem SQL Server as well as a cloud-hosted Postgres, or that offers an embedded/offline mode backed by local storage, has a real reason to keep persistence technology behind a seam. 3. **The third case is when the domain model and the persistence model are meant to diverge** — the domain object shouldn't be the same class as what the ORM persists — in which case the Repository interface's return type (plain domain object, not managed entity) is doing real translation work, not just delegation. ## When to skip it When none of those apply: - simple CRUD screens; - no business-rule complexity worth isolating in a unit test; - no realistic plan to change persistence technology; - the domain model is fine being identical to the persistence model. Then many senior engineers recommend skipping a hand-written, per-entity Repository interface and depending directly on what a well-designed persistence framework already provides. Concretely, this often means using Spring Data JPA's generated `JpaRepository<Entity, Id>` interfaces (which *are* a Repository pattern implementation, just framework-generated rather than hand-rolled) without wrapping them in yet another custom interface, or, for very small services, calling the `EntityManager`/session directly inside a thin service layer. The judgment call is essentially: **is there a real seam here, or am I adding indirection to satisfy a rule rather than to solve an actual problem the codebase has.** ## A real-world instance A recognizable real-world pattern of this exact debate: many Spring Boot tutorials and small production services simply use `JpaRepository<Order, Long>` interfaces directly in service and controller code with no additional custom Repository wrapper on top, which is widely accepted as pragmatic — it's still 'the Repository pattern' (Spring Data's generated proxy fulfills the same collection-like contract), but teams correctly recognize that adding a second, hand-written interface purely to wrap the already-abstract `JpaRepository` for a CRUD-only entity buys nothing. Conversely, teams building a domain-heavy service (e.g., an order-fulfillment engine with complex inventory and pricing rules) commonly do introduce a dedicated, framework-free Repository interface specifically so their extensive business-rule unit test suite never needs a database at all — the exact scenario where the pattern's cost is clearly repaid.

  • If a team decides to skip a custom Repository layer for a simple CRUD service, how do they still get testability for the thin service layer that remains?
    They typically rely on fast, real-database integration tests — an embedded or ephemeral database (via Testcontainers or an in-memory-mode real engine) spun up in seconds — rather than needing a hand-rolled in-memory fake, since the service logic itself is thin enough that testing it against the real persistence framework is fast and reliable enough not to need the extra abstraction.
  • How would you recognize, in a code review, that a Repository interface has become pure ceremony rather than a load-bearing abstraction?
    A strong signal is that every method on the interface has a single, trivial one-line implementation that does nothing but call the equivalent ORM/session method with no additional logic, validation, mapping, or query composition, combined with there being no realistic plan or history of ever swapping the underlying persistence technology or needing an isolated in-memory test double.

Wrapping a already-abstract ORM session in a Repository that just forwards every call is like putting a second universal remote inside the box of a universal remote — it looks like it adds a layer of control, but every button just presses the same button underneath, so you've added a step without adding a capability.

saying these in an interview costs you the question

  • Insists every entity must always have its own Repository interface with no situational judgment
  • Can't articulate any concrete cost of adding the pattern (files, indirection, false abstraction signal)
  • Doesn't recognize that Spring Data's JpaRepository is itself already a Repository pattern implementation
  • Claims the pattern is only ever about testability, missing the multi-backend/domain-model-divergence cases

context