The Difference Between DAO and Repository Patterns: Practical Analysis in DDD and Hibernate

Nov 24, 2025 · Programming · 19 views · 7.8

Keywords: DAO Pattern | Repository Pattern | Domain-Driven Design | Hibernate | Data Access Layer

Abstract: This article provides an in-depth exploration of the core differences between Data Access Object (DAO) and Repository patterns and their applications in Domain-Driven Design (DDD). DAO serves as an abstraction of data persistence, closer to the database layer and typically table-centric, while Repository abstracts a collection of objects, aligning with the domain layer and focusing on aggregate roots. Through detailed code examples, the article demonstrates how to implement these patterns in Hibernate and EJB3 environments, analyzing their distinct roles in unit testing and architectural layering.

Core Concepts of DAO and Repository Patterns

When building enterprise Java applications, the design of the Data Access Layer (DAL) is crucial. The Data Access Object (DAO) pattern and the Repository pattern are two common approaches to abstracting data access, with significant differences in conceptual levels and application scenarios.

The DAO pattern primarily focuses on the technical details of data persistence, providing direct access to underlying data storage. For instance, when using Hibernate ORM, DAOs typically correspond to specific database tables, encapsulating CRUD (Create, Read, Update, Delete) operations. This pattern is closer to the database layer, and its interface design often reflects the database structure rather than business domain concepts.

The Repository pattern, from a Domain-Driven Design (DDD) perspective, abstracts data access as operations on a collection of objects. Repositories operate within the domain layer, handling only aggregate roots, and their interface design uses the domain's ubiquitous language, completely hiding the technical details of data persistence.

Architectural Layers and Design Principles

From an architectural viewpoint, DAOs usually reside in the infrastructure layer, responsible for handling specific data storage technologies (e.g., SQL queries, Hibernate Session management). Their method names often reflect data operation characteristics, such as updateUser() or deleteOrder().

Repositories, however, are part of the domain layer. Their interfaces should remain simple, typically including only collection-like operations such as get(id), find(specification), and add(entity). In the Repository pattern, changes to entity state are usually tracked by a separate UnitOfWork rather than through explicit update methods.

This layering difference is particularly important in Test-Driven Development (TDD). The domain-layer positioning of Repositories makes them easier to unit test, as in-memory implementations or mock objects can be readily created without relying on specific data storage infrastructure.

Implementation Patterns and Code Examples

In practical projects, Repositories are often implemented using DAOs, but the reverse is rare. The following code examples illustrate typical implementations in Hibernate and EJB3 environments.

First, consider a DAO implementation for user data access:

@Stateless
public class UserDAO {
    @PersistenceContext
    private EntityManager entityManager;
    
    public User findById(Long id) {
        return entityManager.find(User.class, id);
    }
    
    public void update(User user) {
        entityManager.merge(user);
    }
    
    public void delete(User user) {
        entityManager.remove(user);
    }
    
    public List<Permission> findPermissionsByUserId(Long userId) {
        String jpql = "SELECT p FROM Permission p WHERE p.user.id = :userId";
        return entityManager.createQuery(jpql, Permission.class)
                           .setParameter("userId", userId)
                           .getResultList();
    }
}

This DAO implementation directly operates the EntityManager, with method designs reflecting database operation characteristics and the ability to handle multiple related entity types (e.g., User and Permission).

In contrast, Repository implementations are more domain-oriented:

public interface UserRepository {
    User get(UserId id);
    List<User> find(Specification<User> specification);
    void add(User user);
    void remove(User user);
}

@Stateless
public class JpaUserRepository implements UserRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public User get(UserId id) {
        return entityManager.find(User.class, id.getValue());
    }
    
    @Override
    public List<User> find(Specification<User> specification) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);
        
        query.where(specification.toPredicate(root, query, cb));
        return entityManager.createQuery(query).getResultList();
    }
    
    @Override
    public void add(User user) {
        entityManager.persist(user);
    }
    
    @Override
    public void remove(User user) {
        entityManager.remove(user);
    }
}

This Repository implementation uses domain concepts (such as the UserId value object and Specification pattern), with an interface design that completely conceals JPA implementation details. Note the absence of an update method, as entity state changes are handled by JPA's automatic dirty checking mechanism.

Combined Usage and Best Practices

In complex domain scenarios, Repositories can compose multiple DAOs to construct aggregate roots. For example, an Order aggregate root might involve multiple data sources like order tables, order item tables, and customer tables:

public class OrderRepositoryImpl implements OrderRepository {
    private final OrderDAO orderDAO;
    private final OrderItemDAO orderItemDAO;
    private final CustomerDAO customerDAO;
    
    @Override
    public Order get(OrderId id) {
        OrderEntity orderEntity = orderDAO.findById(id.getValue());
        List<OrderItemEntity> itemEntities = orderItemDAO.findByOrderId(id.getValue());
        CustomerEntity customerEntity = customerDAO.findById(orderEntity.getCustomerId());
        
        return Order.reconstitute(orderEntity, itemEntities, customerEntity);
    }
}

This design allows the Repository to focus on domain logic while delegating specific data access details to individual DAOs, achieving clear separation of responsibilities.

Common Misconceptions and Considerations

In practice, a common mistake is implementing Repositories as "enhanced DAOs." A true Repository should:

The choice between DAO and Repository depends on specific architectural needs. In simple CRUD applications, DAO may suffice; in complex domain-driven designs, Repository offers better domain abstraction and testing support.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.