The critical difference between the Repository pattern and the Data Access Object pattern is that the former deals with domain concepts, while the latter deals with technology-specific implementation details.
A Repository
interface lives within your domain layer, and thus is defined using the terms in your ubiquitous language. More specifically, the repository deals with an aggregate root and serves as an abstraction over the manipulation of a collection of aggregate roots.
interface OrderRepository {
Collection<Order> findAll();
void save(Order order);
void delete(Order order);
}
Notice that this interface is not coupled to any persistence technology: it simply defines the actions.
The implementation of the Repository
, as in the concrete type which implements the methods defined in the interface, must of course be specific to a certain technology. However, the implementation details of how the aggregate roots is mapped to and from technology-specific structures is abstracted away from your domain layer. Concretely, the implementation would not live within your domain layer, and would instead be defined in a separate package from which your domain layer does not have access. An example implementation could look like this:
class PostgresOrderRepository implements OrderRepository {
@Override
public Collection<Order> findAll() {
Collection<PostgresOrderEntity> orderEntity = getOrdersFromOrderTable();
Collection<PostgresClientEntity> clientEntity = getClientsFromClientTable();
Collection<Order> orders = constructDomainOrderObjectsFromPostgresEntities(...);
return orders;
}
// ...
}
The implementation is responsible to orchestrate the retrieval of the data from PostgreSQL, and it knows from which tables to do so. Once it has all the PostgreSQL entities, it can construct the aggregate root and return it, which means that clients of the repository never have to know what happened under the hood.
For a simple repository, it may be enough to define an implementation which interacts directly with the database tables. There can come a time however where a repository deals with a large number of different tables, all of which support many different queries. Defining every of these operations in a single implementation of a repository may end up dramatically increase its size and complexity. It can be helpful to encapsulate the operations on a single table in a DAO.
The DAO deals with a specific technology and, usually, over a single collection (such as a table in a relation database management system). It does not work with aggregate roots and domain concepts, only technology-specific entities.
class PostgresOrderDao {
public Collection<PostgresOrderEntity> selectAll() {
var query = "SELECT * FROM order";
var rows = connection.query(query);
return rows.stream().map(PostgresOrderEntity::fromRow).toList();
}
The logic as to how to retrieve the data, such as which table it is persisted to, or how to interpret the value of each column, is encapsulated within this implementation. In practice, you will often have one DAO class per table.
Once a DAO implementation is defined, you can use it in the implementation of your repository, which may simplify it a great deal:
class PostgresOrderRepository implements OrderRepository {
// ...
@Override
public Collection<Order> findAll() {
Collection<PostgresOrderEntity> orderEntity = orderDao.selectAll();
Collection<PostgresClientEntity> clientEntity = clientDao.selectAll();
Collection<Order> orders = constructDomainOrderObjectsFromPostgresEntities(...);
return orders;
}
// ...
}
Your implementation of the repository may then be composed with as many DAOs as it needs.
In short,
- The interface of a Repository is defined from your ubiquitous language and is technology-agnostic;
- The implementation of this technology-specific and is responsible for converting to and from your aggregate roots and the technology-specific entities;
- A DAO is technology-specific and deals directly with the technology-specific entities, being only responsible for operations over a collection such as a PostgreSQL table.