Definition
- DAO (Data Access Object) – abstracts and encapsulates access to a specific data source, exposing persistence operations typically mapped to a single table or entity, close to the storage mechanism.
- Repository – a higher-level abstraction from Domain-Driven Design that mediates between the domain and data-mapping layers, presenting a collection-like interface for aggregate roots.
- Spring Data Repository – an interface whose implementation is generated at runtime by Spring Data, eliminating hand-written boilerplate.
- Swappable store – because callers depend on the interface, the backing implementation can be swapped from JPA to MongoDB to in-memory without touching the service.
- Testability – both patterns let you mock or substitute the data-access interface in unit tests, testing business logic with no database.
A DAO is like a specialist filing clerk at National Bank of India who knows exactly which cabinet, drawer, and folder format holds account records, very close to the physical storage. A Repository is like a librarian who gives you a clean collection view: give me the account for Rajesh or find all overdrawn accounts, without you ever knowing whether the data lives in a filing cabinet, a database, or a cloud store. The librarian speaks the language of the business; the clerk speaks the language of the storage.
Understanding the Concepts
What is the DAO pattern?
The DAO (Data Access Object) pattern abstracts access to a specific data source. A DAO typically mirrors a table and exposes storage-oriented operations like insert, findById, update, and delete. It is close to the storage mechanism and often contains SQL or JPA calls directly. You might have one DAO per table and compose them in the service layer.
package com.bank.dao;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.Optional;
public class AccountDao {
@PersistenceContext
private EntityManager em;
public void insert(Account account) {
em.persist(account);
}
public Optional<Account> findById(String accountId) {
Account account = em.find(Account.class, accountId);
return Optional.ofNullable(account);
}
public void update(Account account) {
em.merge(account);
}
public void delete(String accountId) {
Account account = em.find(Account.class, accountId);
if (account != null) {
em.remove(account);
}
}
}
DAO characteristics:
- Storage-oriented: insert, update, delete, findById
- One DAO per table or entity
- Contains JPA/JDBC calls directly
- Developer writes all the implementation code
What is the Repository pattern?
The Repository pattern, from Domain-Driven Design, is a higher-level abstraction that presents a collection-like interface for aggregate roots. Instead of exposing raw CRUD, it expresses domain intent through methods like findOverdrawnAccounts or findByHolderPan. In Spring Data, extending JpaRepository gives you a Repository whose implementation is generated at runtime.
package com.bank.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bank.model.Account;
import java.math.BigDecimal;
import java.util.List;
public interface AccountRepository extends JpaRepository<Account, String> {
// Domain-intent queries: no implementation needed
List<Account> findByBalanceLessThan(BigDecimal threshold);
List<Account> findByHolderPan(String pan);
List<Account> findByStatusAndBranchCode(String status, String branchCode);
}
Repository characteristics:
- Domain-oriented: findOverdrawnAccounts, findByHolderPan
- Interface-based, Spring generates implementation at runtime
- No boilerplate code to write
- Speaks the language of the business, not the storage
How do DAO and Repository differ?
| Aspect | DAO | Repository |
|---|---|---|
| Origin | Core J2EE pattern | Domain-Driven Design |
| Abstraction level | Table/storage-oriented | Domain/aggregate-oriented |
| Implementation | Developer writes it manually | Spring Data generates it at runtime |
| Method naming | insert, update, delete | save, findByX (domain intent) |
| Granularity | One per table | One per aggregate root |
| Boilerplate | Significant | Minimal (just an interface) |
| Domain contract | No aggregate guarantees | Manages aggregate roots as a unit |
// DAO: storage-oriented, manual implementation
package com.bank.dao;
public class AccountDao {
public void insert(Account account) { em.persist(account); }
public void update(Account account) { em.merge(account); }
public void delete(String id) { em.remove(em.find(Account.class, id)); }
}
// Repository: domain-oriented, auto-generated
package com.bank.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bank.model.Account;
public interface AccountRepository extends JpaRepository<Account, String> {
// Spring generates all CRUD + findByX + paging + batch methods
}
How do they enable a swappable data store?
Because callers depend on an interface, you can provide a JPA implementation today and a MongoDB or in-memory implementation tomorrow without touching the service. The key is not leaking store-specific types across the boundary.
// Service depends on the interface, not the implementation
package com.bank.service;
import com.bank.repository.AccountRepository;
import com.bank.model.Account;
import java.math.BigDecimal;
import java.util.List;
public class OverdraftDetectionService {
private final AccountRepository accountRepository;
public OverdraftDetectionService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public List<Account> findOverdrawnAccounts() {
return accountRepository.findByBalanceLessThan(BigDecimal.ZERO);
}
}
Swappable store demonstration:
- Production: Spring Data JPA provides the AccountRepository implementation (PostgreSQL)
- Integration test: H2 in-memory database backs the same interface
- Unit test: a mock or in-memory stub backs the same interface
- Future: MongoDB implementation could replace JPA without changing OverdraftDetectionService
How do they enable testability?
Both patterns let you mock or substitute the data-access interface in unit tests. With a Repository, you can mock it directly because it is an interface. With a DAO, you can depend on an interface and provide a test stub.
package com.bank.service;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import com.bank.repository.AccountRepository;
import com.bank.model.Account;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class OverdraftDetectionServiceTest {
@Test
void findsOverdrawnAccounts() {
AccountRepository mockRepo = mock(AccountRepository.class);
Account overdrawn = new Account("SAV2001", "Rajesh", new BigDecimal("-500.00"));
when(mockRepo.findByBalanceLessThan(BigDecimal.ZERO))
.thenReturn(List.of(overdrawn));
OverdraftDetectionService service = new OverdraftDetectionService(mockRepo);
List<Account> result = service.findOverdrawnAccounts();
assertEquals(1, result.size());
assertEquals("SAV2001", result.get(0).getAccountId());
}
}
Expected test output:
- Test passes with no database connection
- Business logic tested in isolation
- Mock returns predefined data
What are common mistakes?
The most common mistake is leaking store-specific types across the repository boundary. If the repository returns JPA entities with lazy proxies, the abstraction is not truly swappable because the caller depends on JPA behaviour. The fix is to return domain objects or DTOs from the repository boundary.
// WRONG: leaking JPA entity with lazy proxy across the boundary
package com.bank.controller;
import com.bank.repository.AccountRepository;
import com.bank.model.Account;
public class AccountController {
public Account getAccount(String id) {
Account account = accountRepository.findById(id).orElseThrow();
// account.getTrades() might be a Hibernate lazy proxy
// If OSIV is off, this throws LazyInitializationException
// The "swappable store" abstraction is broken
return account;
}
}
// CORRECT: return a DTO from the service layer
package com.bank.service;
import com.bank.repository.AccountRepository;
import com.bank.dto.AccountDto;
public class AccountService {
private final AccountRepository accountRepository;
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public AccountDto getAccount(String id) {
return accountRepository.findById(id)
.map(a -> new AccountDto(a.getAccountId(), a.getHolderName(), a.getBalance()))
.orElseThrow();
// DTO is a plain object, no lazy proxy, truly swappable
}
}
When should you use Repository vs DAO?
Use Spring Data Repository for the common case because it eliminates boilerplate and expresses domain intent. Use a hand-written DAO or a repository custom-implementation fragment when you need fine-grained control over SQL, stored procedures, or a non-JPA data source.
// Custom fragment: combine Repository convenience with DAO control
package com.bank.repository;
import com.bank.model.Account;
import java.util.List;
public interface AccountRepositoryCustom {
List<Account> findByComplexCriteria(String region, String status, int minAge);
}
package com.bank.repository;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
public class AccountRepositoryCustomImpl implements AccountRepositoryCustom {
@PersistenceContext
private EntityManager em;
@Override
public List<Account> findByComplexCriteria(String region, String status, int minAge) {
// Hand-written JPA criteria or native SQL when Spring Data cannot express it
return em.createQuery(
"SELECT a FROM Account a WHERE a.region = :region AND a.status = :status",
Account.class)
.setParameter("region", region)
.setParameter("status", status)
.getResultList();
}
}
What should a senior engineer know?
A senior engineer knows that in strict DDD, a Repository manages whole aggregates, loading and saving an aggregate root as a unit to protect its invariants. A DAO does not carry that domain contract. They also know that hiding the store behind an interface is only truly swappable if you avoid leaking store-specific types. They use Spring Data repositories for the common case and drop to a custom implementation fragment for complex queries.
package com.bank.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bank.model.Account;
// Combine auto-generated methods with custom fragment
public interface AccountRepository
extends JpaRepository<Account, String>, AccountRepositoryCustom {
// Spring Data generates CRUD + paging methods
// AccountRepositoryCustomImpl provides the complex query
}
Visual Overview
DAO Pattern Repository Pattern
============= ==================
+-------------+ +------------------+
| Service | | Service |
| Layer | | Layer |
+------+------+ +--------+---------+
| |
v v
+-------------+ +------------------+
| AccountDao | | AccountRepository|
| (class) | | (interface) |
|-------------| |------------------|
| insert() | | save() |
| update() | | findByPan() |
| delete() | | findOverdrawn() |
| findById() | | (domain intent) |
+------+------+ +--------+---------+
| |
v v
+-------------+ +------------------+
| EntityMgr | | Spring Data impl |
| JDBC / SQL | | (auto-generated) |
+-------------+ +------------------+
| |
v v
Database Database
DAO: developer writes impl Repo: Spring generates impl
DAO: storage language Repo: domain language
Key Takeaway
Both the DAO and Repository patterns exist to decouple business logic from persistence, but they sit at different levels. A DAO abstracts a specific data source and is usually organized table by table, exposing storage-oriented operations like insert, update, and delete. A Repository from Domain-Driven Design is a higher-level abstraction that presents a collection-like view of aggregate roots and speaks the language of the domain, so instead of raw CRUD it exposes intent like find-overdrawn-accounts or find-by-holder-pan. In Spring Data, extending JpaRepository gives Rajesh a Repository whose implementation is generated at runtime, which removes the boilerplate a hand-written DAO would carry.
The three benefits both patterns deliver are abstraction, a swappable store, and testability. Abstraction means the services at National Bank of India depend on an interface rather than on JDBC, JPA, or a vendor. A swappable store follows directly: the same service code runs against PostgreSQL in production and against a HashMap in a unit test. Testability means Priya can mock the repository in unit tests and exercise the business logic with no database at all.
The senior nuance Pankaj stresses is that the abstraction is only genuinely swappable if you do not leak store-specific types across the boundary, so the repository returns domain objects or DTOs rather than JPA entities with lazy proxies. In strict DDD the Repository also manages whole aggregates, loading and saving an aggregate root as a unit to protect its invariants, which is a contract a plain DAO does not carry.
Remember one sentence: a DAO speaks the language of the storage while a Repository speaks the language of the domain, and both decouple business logic from persistence for swappable stores and testability.
---