Una vez decidida la tecnología de persistencia, surge un problema de diseño recurrente: ¿cómo conectamos la lógica de negocio con la base de datos sin que el código se llene de sentencias SQL desperdigadas y dependencias del motor concreto? Si cada parte de la aplicación habla directamente con la base, acabamos con un acoplamiento brutal, código imposible de probar y reglas de negocio mezcladas con detalles de infraestructura. Los patrones de acceso a datos resuelven exactamente esto: introducen una capa que aísla cómo se guardan los datos del qué se hace con ellos. En esta lección estudiaremos los cuatro patrones más importantes (DAO, Repository, Unit of Work) y la dicotomía Data Mapper frente a Active Record, con ejemplos en Java para que veas las diferencias en código real.

Contenido

  1. El problema: acoplamiento a la persistencia
  2. El patrón DAO (Data Access Object)
  3. El patrón Repository
  4. DAO vs Repository: ¿en qué se diferencian?
  5. El patrón Unit of Work
  6. Data Mapper vs Active Record
  7. Cómo encajan todos juntos

  1. El problema: acoplamiento a la persistencia

Imagina un servicio que mezcla lógica de negocio con acceso directo a la base:

public class ServicioPedidos {
    public void confirmar(long pedidoId) throws SQLException {
        Connection con = DriverManager.getConnection("jdbc:postgresql://...");
        PreparedStatement ps = con.prepareStatement(
            "UPDATE pedidos SET estado = 'CONFIRMADO' WHERE id = ?");
        ps.setLong(1, pedidoId);
        ps.executeUpdate();   // lógica de negocio y SQL mezclados
        con.close();
    }
}

Esto tiene problemas graves: el servicio depende de JDBC y de PostgreSQL, es imposible de probar sin una base real, y la sentencia SQL se repetirá en cada sitio que necesite tocar pedidos. Los patrones siguientes separan responsabilidades para evitarlo.

  1. El patrón DAO (Data Access Object)

Un DAO encapsula todo el acceso a una fuente de datos concreta (una tabla, normalmente) y expone operaciones orientadas a la persistencia. Su vocabulario es el de la base de datos: insertar, actualizar, borrar, buscar por clave.

Primero definimos la interfaz, que oculta los detalles de implementación:

public interface PedidoDao {
    void insertar(Pedido pedido);
    void actualizar(Pedido pedido);
    void eliminar(long id);
    Pedido buscarPorId(long id);
    List<Pedido> buscarPorClienteId(long clienteId);
}

Cada método refleja una operación contra la tabla. Ahora una implementación con JDBC:

public class PedidoDaoJdbc implements PedidoDao {

    private final DataSource dataSource;   // pool de conexiones inyectado

    public PedidoDaoJdbc(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Pedido buscarPorId(long id) {
        String sql = "SELECT id, cliente_id, total, estado FROM pedidos WHERE id = ?";
        try (Connection con = dataSource.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {
            ps.setLong(1, id);                       // sustituye el ? por el id
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return new Pedido(
                        rs.getLong("id"),
                        rs.getLong("cliente_id"),
                        rs.getBigDecimal("total"),
                        rs.getString("estado"));
                }
                return null;
            }
        } catch (SQLException e) {
            throw new AccesoDatosException("Error al buscar pedido " + id, e);
        }
    }
    // ... resto de métodos
}

Puntos clave de este código:

  • DataSource inyectado: el DAO no crea la conexión, la recibe. Esto permite cambiar el origen de datos (incluido uno simulado en pruebas).
  • try-with-resources: cierra automáticamente Connection, PreparedStatement y ResultSet aunque haya excepción, evitando fugas de recursos.
  • PreparedStatement con ?: previene inyección SQL y permite reutilizar el plan de ejecución.
  • Traducción de excepciones: convierte la SQLException (de bajo nivel) en una excepción propia, para que el resto de la aplicación no dependa de JDBC.

El DAO centraliza todo el SQL de la tabla pedidos en un único lugar.

  1. El patrón Repository

Un Repository opera a un nivel de abstracción más alto: simula ser una colección en memoria de objetos de dominio. Forma parte del vocabulario del Diseño Dirigido por el Dominio (visto en el Módulo 6) y se asocia a un agregado, no a una tabla. Su intención es que el código de negocio crea estar trabajando con una lista de objetos, ajeno a que detrás hay una base de datos.

public interface RepositorioPedidos {
    void guardar(Pedido pedido);          // "añadir a la colección"
    Optional<Pedido> obtener(PedidoId id);
    List<Pedido> pendientesDe(ClienteId cliente);  // consulta con lenguaje del dominio
}

Diferencias de matiz frente al DAO:

  • Usa identificadores del dominio (PedidoId, ClienteId), no long crudos.
  • Devuelve Optional en lugar de null, expresando explícitamente la ausencia.
  • Los métodos hablan el lenguaje ubicuo del negocio (pendientesDe), no el de SQL.
  • Un único método guardar decide internamente si hay que insertar o actualizar; quien lo llama no lo sabe ni le importa.

Una implementación con Spring Data JPA reduce el código a casi nada:

public interface RepositorioPedidos extends JpaRepository<Pedido, Long> {
    // Spring genera la consulta a partir del nombre del método
    List<Pedido> findByClienteIdAndEstado(Long clienteId, String estado);
}

Aquí JpaRepository ya aporta save, findById, etc., y Spring deriva la consulta del nombre findByClienteIdAndEstado. La intención de negocio queda en el nombre, sin SQL manual.

  1. DAO vs Repository: ¿en qué se diferencian?

Es la confusión más habitual. Ambos abstraen el acceso a datos, pero su intención y nivel difieren:

Aspecto DAO Repository
Orientación A la tabla / fuente de datos Al agregado del dominio
Vocabulario Persistencia (insertar, actualizar) Negocio (colección de objetos)
Granularidad Una por tabla, normalmente Una por agregado raíz
Procedencia Patrón de capa de datos clásico Patrón táctico de DDD
Conoce SQL Sí, lo expone conceptualmente No, lo oculta tras la "colección"

En la práctica, un Repository suele apoyarse en uno o varios DAO o en un ORM. No son excluyentes: el Repository es la cara que ve el dominio, el DAO es la maquinaria interna.

  1. El patrón Unit of Work

¿Qué pasa cuando una operación de negocio modifica varios objetos y todos deben confirmarse juntos? Si llamamos a guardar uno por uno, podríamos confirmar la mitad y fallar en la otra mitad. El Unit of Work resuelve esto: mantiene una lista de objetos afectados durante una operación de negocio y coordina la escritura y la gestión transaccional como una sola unidad atómica.

public class ServicioTransferenciaStock {

    private final RepositorioPedidos repoPedidos;
    private final RepositorioStock repoStock;

    @Transactional   // delimita la Unit of Work: todo o nada
    public void confirmarPedido(PedidoId id) {
        Pedido pedido = repoPedidos.obtener(id)
            .orElseThrow(() -> new PedidoNoEncontradoException(id));

        pedido.confirmar();                  // cambia el estado en memoria
        repoStock.descontar(pedido.lineas()); // descuenta stock en memoria

        repoPedidos.guardar(pedido);         // marca para persistir
        // Al salir del método, @Transactional hace COMMIT de TODO junto;
        // si algo lanza excepción, hace ROLLBACK de TODO.
    }
}

Cómo funciona:

  • La anotación @Transactional de Spring implementa el Unit of Work: abre una transacción al entrar y la confirma (COMMIT) al salir sin error, o la deshace (ROLLBACK) si se lanza una excepción.
  • Las modificaciones a pedido y a stock se acumulan y se aplican atómicamente. No es posible confirmar el pedido sin descontar el stock.
  • En JPA/Hibernate, el EntityManager es en sí mismo una Unit of Work: rastrea los cambios de las entidades cargadas (dirty checking) y los vuelca en el flush.

El Unit of Work aporta dos beneficios: atomicidad (la garantía A de ACID a nivel de aplicación) y eficiencia (agrupa escrituras en un solo viaje a la base en vez de muchos).

  1. Data Mapper vs Active Record

Estos dos patrones describen cómo se relaciona un objeto con su representación en la base.

Active Record: el objeto de dominio contiene su propia lógica de persistencia. La fila y el objeto son la misma cosa.

// Estilo Active Record (típico de frameworks como Ruby on Rails o algunos en Java)
Pedido pedido = Pedido.buscarPorId(42);
pedido.setEstado("CONFIRMADO");
pedido.guardar();   // el propio objeto sabe escribirse en la base

Ventaja: rápido y directo para CRUD sencillo. Inconveniente: mezcla negocio y persistencia en la misma clase, dificultando las pruebas y violando la separación de responsabilidades.

Data Mapper: una capa separada (el "mapeador") traslada entre los objetos de dominio y la base. El objeto de dominio no sabe nada de la persistencia.

// Estilo Data Mapper: el objeto es "ignorante" de la base
Pedido pedido = new Pedido(/* datos puros de negocio */);
pedido.confirmar();              // solo lógica de negocio, sin guardar()
entityManager.persist(pedido);   // el mapper (JPA) se encarga de escribir

Hibernate/JPA son implementaciones de Data Mapper. Comparativa:

Criterio Active Record Data Mapper
Acoplamiento dominio-BD Alto (en la misma clase) Bajo (separados)
Curva de aprendizaje Baja Media
Testabilidad del dominio Limitada Alta (dominio puro)
Idóneo para CRUD simple, prototipos Dominios complejos, DDD

Errores Comunes y Consejos

  • Filtrar excepciones de infraestructura hacia el dominio. Si una SQLException o una JpaException sube hasta el servicio de negocio, has roto el aislamiento. Tradúcelas en el DAO/Repository.
  • Repository "gordo" con cientos de métodos. Si un repositorio crece sin control, probablemente el agregado está mal definido. Un repositorio por agregado raíz.
  • Devolver entidades JPA gestionadas fuera de la transacción. Provoca el temido LazyInitializationException. Devuelve DTO o carga lo necesario dentro de la transacción.
  • Poner @Transactional en métodos privados o llamadas internas de la misma clase. Spring usa proxies; la transacción no se activa en autollamadas. Colócala en el punto de entrada público.
  • Consejo: no abstraigas por abstraer. En aplicaciones pequeñas, Spring Data JPA ya te da Repository + Unit of Work sin escribir DAO a mano. Añade capas solo cuando aporten valor.

Ejercicios

Ejercicio 1. Explica con tus palabras por qué un Repository devuelve Optional<Pedido> en lugar de null, y qué riesgo evita.

Ejercicio 2. Tienes que registrar un nuevo cliente y crear su primer pedido en una sola operación que debe ser atómica. Esboza el método de servicio indicando dónde colocarías la frontera transaccional (Unit of Work).

Ejercicio 3. Clasifica cada situación como Active Record o Data Mapper: (a) la clase Factura tiene un método guardar(); (b) un EntityManager persiste un objeto Factura que solo contiene reglas de negocio.

Soluciones

Solución 1. Optional<Pedido> expresa de forma explícita en el tipo que el pedido puede no existir. Obliga a quien llama a manejar la ausencia (con orElseThrow, map, etc.) y evita los NullPointerException que surgen al usar un null no comprobado. La intención de "puede que no haya resultado" queda documentada en la firma del método.

Solución 2.

@Transactional   // frontera del Unit of Work: ambos guardados se confirman juntos
public void altaClienteConPrimerPedido(DatosCliente datos, DatosPedido linea) {
    Cliente cliente = new Cliente(datos.nombre(), datos.email());
    repoClientes.guardar(cliente);

    Pedido pedido = cliente.crearPedido(linea);
    repoPedidos.guardar(pedido);
    // COMMIT al salir; si falla el segundo guardado, ROLLBACK del cliente también.
}

La frontera transaccional se coloca en el método de servicio (@Transactional), de modo que la creación del cliente y la del pedido formen una única unidad atómica.

Solución 3. (a) Active Record: el objeto Factura contiene su propia lógica de persistencia (guardar()). (b) Data Mapper: la persistencia vive fuera (en el EntityManager) y la Factura solo tiene reglas de negocio.

Conclusión

Has visto cómo aislar la lógica de negocio de los detalles de persistencia mediante cuatro patrones complementarios: el DAO centraliza el SQL por fuente de datos, el Repository ofrece al dominio una colección de objetos en su propio lenguaje, el Unit of Work garantiza la atomicidad de operaciones que tocan varios objetos, y la dicotomía Data Mapper vs Active Record decide cuánto sabe el objeto de dominio sobre su propia persistencia. Frameworks como Spring Data JPA combinan varios de estos patrones por ti. Hasta ahora hemos supuesto una única base de datos; pero en arquitecturas de microservicios cada servicio tiene la suya, lo que abre retos nuevos de consultas y transacciones repartidas. Eso es lo que abordaremos en la siguiente lección: Base de Datos por Servicio y Gestión de Datos Distribuidos.

Curso de Arquitectura de Aplicaciones

Módulo 1: Fundamentos de la Arquitectura de Aplicaciones

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados