Back to Spring Data Repositories
Q2Question 2 of 4

Repository pattern vs DAO?

Key Concepts
  • The Repository pattern abstracts data access behind an interface so the service layer depends on the abstraction, not the implementation
  • Services call tradeRepo.findSettled(accountId) without knowing if data comes from PostgreSQL, MongoDB, or Redis
  • DAO (Data Access Object) is the older pattern with the same goal but typically exposes lower-level methods with visible SQL
  • Spring Data auto-generates the repository implementation at runtime from just an interface definition
  • Benefits: swappable persistence store, easy mocking in unit tests, clean separation of concerns
  • Keep queries in the repository, business logic in the service, HTTP concerns in the controller
  • Repository is DDD-oriented (collection of domain objects); DAO is more data-oriented (CRUD on tables)

Definition

The Repository pattern abstracts data access behind an interface so business logic depends on the abstraction, not the persistence technology – enabling swappable stores, easy mocking, and clean separation.
  • Repository pattern – an interface that abstracts data access. The service calls tradeRepo.findSettled(accountId) without knowing the implementation. Follows the Dependency Inversion Principle.
  • DAO (Data Access Object) – the older pattern with the same goal. Typically more data-oriented and may expose lower-level methods with visible SQL or JDBC.
  • Spring Data Repository – you define an interface, Spring auto-generates the implementation at runtime. Method names are parsed into queries automatically.
  • Swappable store – change from JPA to MongoDB by creating a new implementation; the service does not change.
  • Testability – mock the repository interface in unit tests. No database needed. Tests run in milliseconds.
  • Separation – queries live in the repository, business logic in the service, HTTP concerns in the controller.

At National Bank of India, the service layer is like the branch manager. The manager says "give me all settled trades for Rajesh" without caring if the clerk looks in a filing cabinet (PostgreSQL), a digital ledger (MongoDB), or a memory cache (Redis). The Repository is the counter window: the manager walks up and asks for data. Behind the counter, the implementation handles the details. DAO is like an older counter that shows more plumbing – the raw SQL is visible.

Understanding the Concepts

What is the Repository pattern?

The Repository pattern follows the Dependency Inversion Principle: high-level modules (service) depend on abstractions (repository interface), not low-level modules (JPA implementation). The service never imports JPA, EntityManager, or SQL.

JAVA
package com.bank.repository;

import com.bank.model.Trade;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

// Repository interface: defines WHAT, not HOW
public interface TradeRepository {
    List<Trade> findSettledByAccountId(String accountId);
    Optional<Trade> findById(String tradeId);
    Trade save(Trade trade);
    void deleteById(String tradeId);
}
// Service depends on THIS interface.
// Does not know about JPA, SQL, or MongoDB.
JAVA
package com.bank.service;

import com.bank.model.Trade;
import com.bank.repository.TradeRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class SettlementService {

    private final TradeRepository tradeRepo;  // interface, not implementation!

    public SettlementService(TradeRepository tradeRepo) {
        this.tradeRepo = tradeRepo;
    }

    @Transactional(readOnly = true)
    public BigDecimal calculateSettledTotal(String accountId) {
        return tradeRepo.findSettledByAccountId(accountId).stream()
            .map(Trade::getAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

How does Spring Data implement the Repository pattern?

With Spring Data, you define an interface and Spring generates the implementation class at runtime. Method names are parsed into JPQL queries automatically.

JAVA
package com.bank.repository;

import com.bank.model.Trade;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

// Spring Data auto-generates the implementation!
public interface JpaTradeRepository extends JpaRepository<Trade, String> {
    List<Trade> findByAccountIdAndStatus(String accountId, String status);
    // Spring parses the method name and generates:
    // SELECT t FROM Trade t WHERE t.accountId = ?1 AND t.status = ?2
    // No SQL written by the developer!
}

How is Repository different from DAO?

Both abstract data access. DAO is more data-oriented and traditionally exposes CRUD at a lower level. Repository is DDD-oriented (Domain-Driven Design) – it acts as a collection of domain objects.

| Aspect | Repository | DAO |
|--------|-----------|-----|
| Origin | Domain-Driven Design | Core J2EE Patterns |
| Abstraction | Collection of domain objects | CRUD on tables |
| Implementation | Spring auto-generates | Developer writes manually |
| SQL visibility | Hidden (derived queries) | Often visible (JDBC, SQL) |
| Technology | Spring Data (JPA, Mongo, Redis) | Typically JDBC or JPA |

JAVA
package com.bank.dao;

import com.bank.model.Trade;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;

// DAO: lower-level, SQL visible
@Repository
public class TradeDao {

    private final JdbcTemplate jdbc;

    public TradeDao(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public List<Trade> findSettled(String accountId) {
        return jdbc.query(
            "SELECT * FROM trades WHERE account_id = ? AND status = 'SETTLED'",
            new TradeRowMapper(), accountId);
        // SQL is visible. DAO knows about JDBC.
    }
}
JAVA
package com.bank.repository;

import com.bank.model.Trade;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

// Repository: higher-level, no SQL
public interface TradeRepository extends JpaRepository<Trade, String> {
    List<Trade> findByAccountIdAndStatus(String accountId, String status);
    // No SQL! Spring generates the query from the method name.
}

Why keep queries out of controllers and services?

Controllers handle HTTP concerns (request parsing, response formatting). Services handle business logic (calculations, rules). Repositories handle data access (queries, persistence). Mixing these violates single responsibility.

JAVA
package com.bank.controller;

import com.bank.model.Trade;
import com.bank.service.TradeService;
import jakarta.persistence.EntityManager;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
public class TradeController {

    private final TradeService tradeService;

    public TradeController(TradeService tradeService) {
        this.tradeService = tradeService;
    }

    // WRONG: query in controller
    // @GetMapping("/trades")
    // public List<Trade> getTrades(@RequestParam String accountId,
    //                              EntityManager em) {
    //     return em.createQuery(
    //         "SELECT t FROM Trade t WHERE t.accountId = :id", Trade.class)
    //         .setParameter("id", accountId).getResultList();
    //     // Controller knows about EntityManager + JPQL. Tight coupling!
    // }

    // CORRECT: query in repository, logic in service
    @GetMapping("/trades")
    public List<Trade> getTrades(@RequestParam String accountId) {
        return tradeService.getTradesForAccount(accountId);
        // Controller knows NOTHING about persistence.
    }
}

How does the Repository pattern help with testing?

With a repository interface, service unit tests mock the repository. No database needed. Tests run in milliseconds.

JAVA
package com.bank.service;

import com.bank.model.Trade;
import com.bank.repository.TradeRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SettlementServiceTest {

    @Mock
    private TradeRepository tradeRepo;

    @InjectMocks
    private SettlementService service;

    @Test
    void shouldCalculateSettledTotal() {
        Trade t1 = new Trade("TRD-001", "RELIANCE", new BigDecimal("100000"));
        Trade t2 = new Trade("TRD-002", "HDFC", new BigDecimal("200000"));

        when(tradeRepo.findSettledByAccountId("SAV2001"))
            .thenReturn(List.of(t1, t2));

        BigDecimal total = service.calculateSettledTotal("SAV2001");

        assertThat(total).isEqualByComparingTo("300000");
        // NO database! Mock returns test data.
        // Test runs in 10ms.
    }
}

How does swapping the persistence store work?

Because the service depends on the interface, you can create new implementations for different stores without changing the service.

JAVA
package com.bank.repository;

import com.bank.model.Trade;
import java.util.List;
import java.util.Optional;

// Same interface, different implementations:

// JPA implementation (generated by Spring Data)
// public interface JpaTradeRepo extends JpaRepository<Trade, String> {}

// Redis cache implementation (manual)
// public class RedisTradeRepository implements TradeRepository {
//     public List<Trade> findSettledByAccountId(String id) {
//         return redisTemplate.opsForList().range("trades:" + id, 0, -1);
//     }
// }

// In-memory implementation (for tests)
// public class InMemoryTradeRepository implements TradeRepository {
//     private final Map<String, Trade> store = new HashMap<>();
//     public List<Trade> findSettledByAccountId(String id) {
//         return store.values().stream()
//             .filter(t -> t.getAccountId().equals(id))
//             .toList();
//     }
// }

What are common mistakes with repositories?

The most common mistake is leaking persistence concerns into the service or controller layer.

JAVA
package com.bank.service;

import com.bank.model.Trade;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class TradeMistakeService {

    // WRONG: service depends on EntityManager (JPA-specific)
    // private final EntityManager em;
    // public List<Trade> findSettled(String accountId) {
    //     return em.createQuery(...).getResultList();
    //     // Service is coupled to JPA. Cannot mock easily.
    // }

    // CORRECT: service depends on repository interface
    private final com.bank.repository.TradeRepository tradeRepo;

    public TradeMistakeService(com.bank.repository.TradeRepository tradeRepo) {
        this.tradeRepo = tradeRepo;
    }

    public List<Trade> findSettled(String accountId) {
        return tradeRepo.findSettledByAccountId(accountId);
        // Service has zero imports of JPA or EntityManager.
        // Easy to mock. Easy to swap implementation.
    }
}

What should a senior engineer know?

A senior engineer knows that the Repository pattern is not just about separating SQL from business logic. It is about defining a contract between the domain layer and the infrastructure layer. The repository interface uses domain language (findSettledByAccountId) not database language (SELECT * FROM trades WHERE...).

JAVA
package com.bank.repository;

import com.bank.model.Trade;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

// Domain-oriented naming (Repository):
public interface TradeRepository {
    List<Trade> findSettledByAccountId(String accountId);
    List<Trade> findPendingForSettlementDate(LocalDate date);
    BigDecimal calculateTotalExposure(String accountId);
}

// Data-oriented naming (DAO):
// public interface TradeDao {
//     List<Trade> selectByAccountIdAndStatus(String id, String status);
//     List<Trade> selectByDateAndStatus(LocalDate date, String status);
//     BigDecimal sumAmountByAccountId(String id);
// }

Visual Overview

DIAGRAM
  Separation of Concerns:
  ┌───────────────────┐
  │    Controller       │  HTTP concerns (request/response)
  │    (REST API)       │  No queries, no business logic
  └─────────┬─────────┘
            │
            ▼
  ┌───────────────────┐
  │    Service          │  Business logic (calculations, rules)
  │    (@Transactional)  │  Depends on Repository INTERFACE
  └─────────┬─────────┘
            │
            ▼
  ┌───────────────────┐
  │  Repository         │  Data access (queries, persistence)
  │  (Interface)         │  Implementation auto-generated
  └─────────┬─────────┘
            │
            ▼
  ┌───────────────────┐
  │  Database           │  PostgreSQL / MongoDB / Redis
  └───────────────────┘

Key Takeaway

The Repository pattern abstracts data access behind an interface so the service layer depends on the abstraction, not the implementation. The service calls tradeRepo.findSettledByAccountId(accountId) without knowing if the data comes from PostgreSQL, MongoDB, or Redis. This enables three things: swappable persistence technology, easy mocking in unit tests, and clean separation of concerns.

At National Bank of India, Rajesh's SettlementService depends on the TradeRepository interface – it has zero imports of JPA, EntityManager, or SQL. The unit test mocks the repository with Mockito and tests business logic in 10ms without any database. The integration test uses @DataJpaTest to verify the actual queries against H2.

DAO and Repository serve the same purpose: abstracting data access. DAO is the older, more data-oriented term that often exposes SQL or JDBC details. Repository is DDD-oriented, acting as a collection of domain objects with domain-language method names. With Spring Data, the distinction is mostly theoretical – JpaRepository extends CrudRepository and provides both DAO-level CRUD and Repository-level domain abstraction.

Remember one sentence: the Repository pattern lets the service ask for data in domain language while the implementation decides how to get it from the database.

---

Share
Use ← → arrow keys to navigate