Implement DAO in Java with an explicit persistence boundary, focused query methods, and a clear separation between storage concerns and application logic.
DAO: A boundary object that encapsulates persistence operations so the rest of the application does not need to know query, connection, or storage-specific details.
In Java, a DAO is useful when it gives the codebase a clear persistence boundary. It is less useful when it is only another layer that forwards every call to an ORM with no added design value.
Good DAOs expose operations in the language of the application, not in the language of SQL tables:
1public interface CustomerDao {
2 Optional<CustomerRecord> findById(CustomerId id);
3 List<CustomerRecord> findActiveByRegion(String region);
4 void save(CustomerRecord customer);
5}
That interface says what the application needs from persistence without exposing connection handling, query strings, or framework mechanics.
1public final class JdbcCustomerDao implements CustomerDao {
2 private final DataSource dataSource;
3
4 public JdbcCustomerDao(DataSource dataSource) {
5 this.dataSource = dataSource;
6 }
7
8 @Override
9 public Optional<CustomerRecord> findById(CustomerId id) {
10 String sql = """
11 select id, name, region, active
12 from customer
13 where id = ?
14 """;
15
16 try (Connection connection = dataSource.getConnection();
17 PreparedStatement statement = connection.prepareStatement(sql)) {
18 statement.setString(1, id.value());
19 try (ResultSet rs = statement.executeQuery()) {
20 if (!rs.next()) {
21 return Optional.empty();
22 }
23 return Optional.of(
24 new CustomerRecord(
25 new CustomerId(rs.getString("id")),
26 rs.getString("name"),
27 rs.getString("region"),
28 rs.getBoolean("active")
29 )
30 );
31 }
32 } catch (SQLException e) {
33 throw new CustomerPersistenceException("Failed to load customer " + id, e);
34 }
35 }
36}
The point is not that JDBC is always the best option. The point is that the DAO owns persistence mechanics, exception translation, and row mapping.
The DAO should isolate:
The visual below shows the intended boundary:
flowchart LR
Service["Application service"] --> DAO["CustomerDao"]
DAO --> SQL["SQL or ORM query"]
SQL --> Store["Database"]
A DAO loses value when it leaks too much of the underlying persistence model:
ResultSet objectsIf those details still dominate service code, the DAO is not really acting as a boundary.
DAO is a strong fit when:
When reviewing a Java DAO, ask:
DAO is strongest when it creates a real persistence boundary. It is weak when it exists only to preserve an old pattern checklist.