Arquitectura de Software

Domain-Driven Design: Repositories para persistir Aggregates sin acoplar el dominio

Diseña un OrderRepository independiente del almacenamiento y compáralo con un DAO, un ORM y un Application Service. Implementa adaptadores en memoria y SQLite, recupera Aggregates completos y comprueba guardado, conflictos de versión y rollback con pruebas ejecutables.

Cargando tus opciones de aprendizaje…

Un pedido se confirma correctamente en memoria. El backend actualiza su cabecera y después guarda las líneas, pero la segunda operación falla. La siguiente lectura encuentra un pedido confirmado con datos anteriores o incompletos.

En otro recorrido, un servicio recupera directamente un modelo del ORM y modifica sus propiedades. El resto de la aplicación necesita conocer relaciones, sesiones y métodos particulares de esa herramienta para trabajar con el pedido.

Un Repository busca ofrecer una forma de recuperar y persistir objetos del Domain sin obligar a sus consumidores a conocer esos mecanismos. Para que la separación sea útil, el contrato debe explicar también qué guarda, qué devuelve y qué ocurre cuando existe un conflicto.

Qué representa un Repository en DDD

Un Repository ofrece acceso a objetos del Domain con una interfaz semejante a una colección, ocultando el mapeo y el acceso al almacenamiento. Fowler describe esa mediación en su patrón Repository, y Evans la desarrolla en su referencia de DDD.

En un modelo con Aggregates, normalmente se diseña alrededor de sus Aggregate Roots: los puntos de entrada que controlan las modificaciones del conjunto. OrderRepository recupera un Order listo para ejecutar sus operaciones, no una cabecera sin las líneas necesarias para comprobar sus reglas.

Esto no significa cargar todos los datos relacionados con una venta. El pedido puede conservar referencias a cliente y producto por identidad, sin reconstruir sus perfiles completos.

El contrato puede definirse en Domain o en el límite de aplicación que lo necesita, según la organización elegida. La implementación concreta pertenece a Infrastructure. Lo esencial es que el consumidor dependa de una abstracción propia, no de una sesión, conexión o tipo del ORM.

Repository, DAO, ORM y Application Service

Un DAO, o Data Access Object, abstrae mecanismos de acceso a una fuente de datos. La definición original de Oracle no lo limita necesariamente a una tabla. La diferencia útil es el contrato: en DDD, el Repository se expresa en objetos y necesidades del modelo, mientras un DAO puede ofrecer operaciones centradas en registros o consultas.

Un ORM, u Object-Relational Mapper, es una herramienta de mapeo entre objetos y almacenamiento relacional. Puede utilizarse dentro de un Repository. No sustituye automáticamente el diseño del contrato ni garantiza que los objetos recuperados coincidan con tus Aggregates.

Un Application Service coordina un Use Case. Por ejemplo: recuperar el pedido, pedirle que se confirme y guardar. La regla para confirmar pertenece al modelo; la consulta SQL pertenece al adaptador. El Repository no debería calcular descuentos ni decidir qué pedidos merecen aprobarse.

La separación se ve en este recorrido:

ConfirmOrder -> OrderRepository [contrato]
                    ^
                    | implementan
             MemoryOrderRepository
             SqliteOrderRepository

Ambos recuperan Order; Order no conoce SQL ni SQLite.

Un contrato pequeño con garantías explícitas

Usaremos tres operaciones: add, para insertar un Aggregate nuevo; findById, para recuperar uno existente; y save, para guardar una actualización condicionada a la versión leída.

No haremos un save ambiguo que inserte o actualice sin distinguir intención. En este ejemplo, un identificador duplicado al añadir es un error; guardar una versión obsoleta también lo es. Un fallo técnico no se traduce a «pedido inexistente».

La versión viajará junto al Aggregate como metadato de persistencia. No es una columna SQL expuesta al Domain ni una regla comercial del pedido: es un número que el contrato utiliza para detectar actualizaciones incompatibles. Otro diseño podría usar un token opaco equivalente.

Ejemplo completo: el mismo contrato, dos implementaciones

Preparación y alcance

El proyecto incluye todos los archivos siguientes y no requiere instalar paquetes:

repository-example/
  package.json
  domain.ts
  application.ts
  memory-repository.ts
  sqlite-repository.ts
  repository.test.ts

Contenido de package.json:

{"name":"repository-example","private":true,"type":"module"}

Usaremos Node.js 24.21.0 y su módulo integrado node:sqlite. El adaptador relacional utiliza DatabaseSync, que ejecuta consultas de forma síncrona; la interfaz del Repository conserva métodos asíncronos para representar el contrato sin atarlo a ese mecanismo. Esta implementación didáctica no es una recomendación de concurrencia o rendimiento para un servidor de producción. Consulta la documentación de SQLite en Node.js al adaptar las APIs a otra versión.

Las pruebas crean una base SQLite real con :memory:: se ejecuta SQL y se comprueban transacciones, pero los datos no sobreviven al cierre. No se modifica una base de datos existente.

1. Domain y contrato independiente

Archivo domain.ts:

export type OrderSnapshot = {
  id: string;
  status: 'draft' | 'confirmed';
  lines: { id: string; productId: string; quantity: number; unitPriceMinorUnits: number }[];
};

class Money {
  readonly minorUnits: number;
  constructor(minorUnits: number) {
    if (!Number.isSafeInteger(minorUnits) || minorUnits < 0) throw new Error('Importe inválido');
    this.minorUnits = minorUnits;
    Object.freeze(this);
  }
}

export class Order {
  #state: OrderSnapshot;

  private constructor(state: OrderSnapshot) {
    const validId = (value: string) => typeof value === 'string' && value.trim().length > 0;
    if (!validId(state.id) || !['draft', 'confirmed'].includes(state.status)) {
      throw new Error('Pedido inválido');
    }
    if (state.lines.length === 0) throw new Error('El pedido necesita líneas');
    const ids = new Set<string>();
    const products = new Set<string>();
    let total = 0;
    for (const line of state.lines) {
      if (!validId(line.id) || !validId(line.productId) || ids.has(line.id) ||
        products.has(line.productId) || !Number.isSafeInteger(line.quantity) || line.quantity <= 0) {
        throw new Error('Línea inválida o duplicada');
      }
      const price = new Money(line.unitPriceMinorUnits);
      total = new Money(total + new Money(price.minorUnits * line.quantity).minorUnits).minorUnits;
      ids.add(line.id);
      products.add(line.productId);
    }
    this.#state = structuredClone(state);
  }

  static start(id: string, lines: OrderSnapshot['lines']): Order {
    return new Order({ id, status: 'draft', lines });
  }

  static restore(state: OrderSnapshot): Order { return new Order(state); }

  confirm(): void {
    if (this.#state.status !== 'draft') throw new Error('Pedido ya confirmado');
    this.#state = { ...this.#state, status: 'confirmed' };
  }

  snapshot(): OrderSnapshot { return structuredClone(this.#state); }
}

export class OrderAlreadyExists extends Error {}
export class OrderWriteConflict extends Error {}

export interface OrderRepository {
  add(order: Order): Promise<number>;
  findById(id: string): Promise<{ order: Order; version: number } | null>;
  save(order: Order, expectedVersion: number): Promise<number>;
}

Los precios son unidades menores enteras de EUR y las líneas no se pueden repetir. El modelo mínimo evita repetir el diseño completo del Aggregate. Sus métodos no contienen SQL, conexiones ni decoradores de persistencia.

restore() conserva identidad y estado, en lugar de iniciar otro pedido en draft. Las entradas son estructuras conocidas; no es un parser general de JSON. Un adaptador externo debe validar la forma recibida y el modelo comprueba las reglas que le corresponden.

2. Implementación en memoria que no comparte referencias

Archivo memory-repository.ts:

import {
  Order, OrderAlreadyExists, OrderWriteConflict,
  type OrderRepository, type OrderSnapshot,
} from './domain.ts';

export class MemoryOrderRepository implements OrderRepository {
  #rows = new Map<string, { state: OrderSnapshot; version: number }>();

  async add(order: Order): Promise<number> {
    const state = order.snapshot();
    if (this.#rows.has(state.id)) throw new OrderAlreadyExists('Pedido duplicado');
    this.#rows.set(state.id, { state, version: 0 });
    return 0;
  }

  async findById(id: string) {
    const row = this.#rows.get(id);
    return row ? { order: Order.restore(row.state), version: row.version } : null;
  }

  async save(order: Order, expectedVersion: number): Promise<number> {
    const state = order.snapshot();
    const current = this.#rows.get(state.id);
    if (!current || current.version !== expectedVersion) {
      throw new OrderWriteConflict('El pedido cambió o ya no existe');
    }
    const version = expectedVersion + 1;
    this.#rows.set(state.id, { state, version });
    return version;
  }
}

Guardar snapshots y reconstruir instancias evita que order.confirm() cambie silenciosamente el almacenamiento antes de llamar a save. La comparación y la sustitución no contienen un await entre ellas; esta simulación solo coordina el estado de su instancia en un proceso.

3. Un Use Case recibe el contrato

Archivo application.ts:

import type { OrderRepository } from './domain.ts';

export class ConfirmOrder {
  #orders: OrderRepository;
  constructor(orders: OrderRepository) { this.#orders = orders; }

  async execute(orderId: string) {
    const loaded = await this.#orders.findById(orderId);
    if (!loaded) throw new Error('Pedido inexistente');
    loaded.order.confirm();
    const version = await this.#orders.save(loaded.order, loaded.version);
    return { id: orderId, status: loaded.order.snapshot().status, version };
  }
}

Pasar el Repository por el constructor es Dependency Injection sencilla. Cambiar la implementación no exige modificar el Use Case. Un controlador podría traducir sus resultados y errores a HTTP; ese transporte queda fuera del ejemplo.

4. Persistencia relacional y mapeo explícito

Archivo sqlite-repository.ts:

import { DatabaseSync } from 'node:sqlite';
import {
  Order, OrderAlreadyExists, OrderWriteConflict,
  type OrderRepository, type OrderSnapshot,
} from './domain.ts';

export function createDemoDatabase(): DatabaseSync {
  const db = new DatabaseSync(':memory:');
  db.exec(`
    PRAGMA foreign_keys = ON;
    CREATE TABLE orders (
      id TEXT PRIMARY KEY,
      status TEXT NOT NULL CHECK (status IN ('draft', 'confirmed')),
      version INTEGER NOT NULL CHECK (version >= 0)
    );
    CREATE TABLE order_lines (
      order_id TEXT NOT NULL REFERENCES orders(id),
      line_id TEXT NOT NULL,
      product_id TEXT NOT NULL,
      quantity INTEGER NOT NULL CHECK (quantity > 0),
      unit_price_minor INTEGER NOT NULL CHECK (unit_price_minor >= 0),
      PRIMARY KEY (order_id, line_id),
      UNIQUE (order_id, product_id)
    );
  `);
  return db;
}

type JoinedRow = {
  order_id: string;
  status: OrderSnapshot['status'];
  version: number;
  line_id: string | null;
  product_id: string;
  quantity: number;
  unit_price_minor: number;
};

export class SqliteOrderRepository implements OrderRepository {
  #db: DatabaseSync;
  constructor(db: DatabaseSync) { this.#db = db; }

  #transaction<T>(work: () => T): T {
    this.#db.exec('BEGIN IMMEDIATE');
    try {
      const result = work();
      this.#db.exec('COMMIT');
      return result;
    } catch (error) {
      this.#db.exec('ROLLBACK');
      throw error;
    }
  }

  #insertLines(state: OrderSnapshot): void {
    const insert = this.#db.prepare(`INSERT INTO order_lines
      (order_id, line_id, product_id, quantity, unit_price_minor) VALUES (?, ?, ?, ?, ?)`);
    for (const line of state.lines) {
      insert.run(state.id, line.id, line.productId, line.quantity, line.unitPriceMinorUnits);
    }
  }

  async add(order: Order): Promise<number> {
    const state = order.snapshot();
    return this.#transaction(() => {
      if (this.#db.prepare('SELECT id FROM orders WHERE id = ?').get(state.id)) {
        throw new OrderAlreadyExists('Pedido duplicado');
      }
      this.#db.prepare('INSERT INTO orders (id, status, version) VALUES (?, ?, 0)')
        .run(state.id, state.status);
      this.#insertLines(state);
      return 0;
    });
  }

  async findById(id: string) {
    const rows = this.#db.prepare(`
      SELECT o.id AS order_id, o.status, o.version,
             l.line_id, l.product_id, l.quantity, l.unit_price_minor
      FROM orders o LEFT JOIN order_lines l ON l.order_id = o.id
      WHERE o.id = ? ORDER BY l.line_id
    `).all(id) as unknown as JoinedRow[];
    if (rows.length === 0) return null;
    const first = rows[0];
    if (!Number.isSafeInteger(first.version) || first.version < 0) {
      throw new Error('Versión persistida inválida');
    }
    const state: OrderSnapshot = {
      id: first.order_id, status: first.status,
      lines: rows.filter(row => row.line_id !== null).map(row => ({
        id: row.line_id as string, productId: row.product_id,
        quantity: row.quantity, unitPriceMinorUnits: row.unit_price_minor,
      })),
    };
    return { order: Order.restore(state), version: first.version };
  }

  async save(order: Order, expectedVersion: number): Promise<number> {
    const state = order.snapshot();
    return this.#transaction(() => {
      const result = this.#db.prepare(`UPDATE orders SET status = ?, version = version + 1
        WHERE id = ? AND version = ?`).run(state.status, state.id, expectedVersion);
      if (result.changes !== 1) throw new OrderWriteConflict('El pedido cambió o ya no existe');
      // Reemplazamos solo las líneas de este pedido, dentro de la misma transacción.
      this.#db.prepare('DELETE FROM order_lines WHERE order_id = ?').run(state.id);
      this.#insertLines(state);
      return expectedVersion + 1;
    });
  }
}

El mapeo traduce unit_price_minor a unitPriceMinorUnits y entrega un Aggregate reconstruido. La consulta conjunta obtiene cabecera y líneas en una sentencia, evitando combinar dos lecturas de momentos diferentes. Las conversiones as describen tipos para TypeScript; no validan datos por sí mismas. El esquema controlado y las comprobaciones del modelo y de versión sostienen este ejemplo.

El orden de líneas se define aquí por identificador al recuperar. Si el negocio necesita un orden de presentación distinto, persiste y mapea una posición explícita; no dependas del orden accidental de una tabla.

Las consultas usan parámetros y no concatenan identificadores recibidos dentro del SQL. El borrado acotado de líneas forma parte de la estrategia de actualización de esta demostración. En un modelo con auditoría, referencias externas o muchas líneas quizá necesites calcular diferencias en vez de reemplazarlas.

BEGIN IMMEDIATE, COMMIT y ROLLBACK siguen las reglas de transacciones de SQLite. Esta conexión no debe tener ya una transacción abierta y no se comparte para operaciones intercaladas. No copies ese control sin adaptarlo al cliente y al manejo de conexiones de tu aplicación.

5. Pruebas del contrato y de rollback

Archivo repository.test.ts:

import assert from 'node:assert/strict';
import test from 'node:test';
import { Order, OrderAlreadyExists, OrderWriteConflict, type OrderRepository } from './domain.ts';
import { ConfirmOrder } from './application.ts';
import { MemoryOrderRepository } from './memory-repository.ts';
import { createDemoDatabase, SqliteOrderRepository } from './sqlite-repository.ts';

function exampleOrder() {
  return Order.start('order-1', [
    { id: 'line-1', productId: 'book', quantity: 2, unitPriceMinorUnits: 1200 },
    { id: 'line-2', productId: 'notebook', quantity: 1, unitPriceMinorUnits: 300 },
  ]);
}

const implementations: [string, () => { repository: OrderRepository; close(): void }][] = [
  ['memoria', () => ({ repository: new MemoryOrderRepository(), close() {} })],
  ['SQLite', () => {
    const db = createDemoDatabase();
    return { repository: new SqliteOrderRepository(db), close: () => db.close() };
  }],
];

for (const [name, create] of implementations) {
  test(`${name}: recuperar, guardar y detectar versiones obsoletas`, async () => {
    const { repository, close } = create();
    try {
      assert.equal(await repository.findById('missing'), null);
      const original = exampleOrder();
      assert.equal(await repository.add(original), 0);
      await assert.rejects(() => repository.add(original), OrderAlreadyExists);

      const stale = await repository.findById('order-1');
      assert.ok(stale);
      original.confirm();
      assert.equal((await repository.findById('order-1'))?.order.snapshot().status, 'draft');

      assert.deepEqual(await new ConfirmOrder(repository).execute('order-1'), {
        id: 'order-1', status: 'confirmed', version: 1,
      });
      const saved = await repository.findById('order-1');
      assert.ok(saved);
      assert.equal(saved.order.snapshot().status, 'confirmed');
      assert.deepEqual(saved.order.snapshot().lines, original.snapshot().lines);
      await assert.rejects(() => repository.save(stale.order, stale.version), OrderWriteConflict);
      assert.equal((await repository.findById('order-1'))?.version, 1);
    } finally { close(); }
  });
}

test('SQLite revierte cabecera, versión y líneas si falla la escritura de una línea', async () => {
  const db = createDemoDatabase();
  const repository = new SqliteOrderRepository(db);
  try {
    await repository.add(exampleOrder());
    // Fallo deliberado de almacenamiento para comprobar atomicidad, no una regla del negocio.
    db.exec(`CREATE TRIGGER fail_line BEFORE INSERT ON order_lines
      WHEN NEW.line_id = 'line-2'
      BEGIN SELECT RAISE(ABORT, 'simulated storage failure'); END;`);
    const loaded = await repository.findById('order-1');
    assert.ok(loaded);
    loaded.order.confirm();
    await assert.rejects(() => repository.save(loaded.order, loaded.version), /simulated/);
    const unchanged = await repository.findById('order-1');
    assert.ok(unchanged);
    assert.equal(unchanged.version, 0);
    assert.deepEqual(unchanged.order.snapshot(), exampleOrder().snapshot());
  } finally { db.close(); }
});

Ejecuta:

node --test repository.test.ts

Las primeras pruebas ejecutan el mismo contrato contra dos mecanismos. El Use Case confirma el pedido sin saber cuál utiliza. La prueba de fallo demuestra que una excepción al insertar la segunda línea no deja confirmada la cabecera ni incrementada la versión.

Las tres pruebas se ejecutaron y pasaron con Node.js 24.21.0 y la biblioteca SQLite integrada en ese entorno. No se realizó análisis estático con el compilador de TypeScript ni se probó otro motor relacional.

La prueba de versión usa dos lecturas lógicamente obsoletas, no una carga concurrente de varios procesos. Comprueba el rechazo condicionado a versión; no mide aislamiento, contención ni rendimiento en producción.

Concurrencia y Unit of Work: qué garantía ofrece el guardado

save(order, expectedVersion) utiliza Optimistic Concurrency Control: solo acepta una actualización si la versión sigue siendo la leída. La comprobación y el guardado de todos los componentes ocurren dentro de la transacción. Un conflicto no debe convertirse en un save sin condición para «hacerlo funcionar».

Si reintentas, vuelve a recuperar el Aggregate y ejecutar la operación con el estado actual. Además, comprueba si el Use Case puede repetirse: una escritura rechazada no deshace efectos externos que ya hayas ejecutado.

Una Unit of Work registra los objetos afectados por una operación y coordina la escritura de cambios y problemas de concurrencia, como define Fowler. No es simplemente otro nombre para BEGIN y COMMIT.

En nuestro diseño, cada add o save completa su propia transacción. Si una operación necesitara coordinar más escrituras locales, tendrías que establecer un límite común y hacer participar a los adaptadores. No basta con invocar varios save que confirman por separado, ni con anidar transacciones que el motor no admite.

Educación: recuperar para decidir, consultar para mostrar

Una academia necesita aprobar una inscripción y listar miles de participantes. La aprobación requiere recuperar el Aggregate con la información necesaria para sus reglas. El listado puede utilizar una consulta paginada que devuelve identificador, nombre visible y estado.

Forzar a EnrollmentRepository a reconstruir cada inscripción para mostrar tres columnas puede añadir trabajo innecesario. Un lector específico puede devolver un DTO, una estructura para transferir datos, sin ofrecer operaciones de modificación.

Separar esos recorridos no exige implantar CQRS ni otro sistema completo. Este ejemplo enseña a distinguir una lectura destinada a ejecutar comportamiento de una proyección destinada a presentación.

Logística: una consulta externa no es persistir un Aggregate local

Una integración consulta el estado de un transportista. El adaptador HTTP puede validar y traducir su respuesta; llamarlo ShipmentRepository no lo convierte automáticamente en una colección de Aggregates propios con las mismas garantías que tu base de datos.

Define si necesitas consultar evidencia externa o recuperar un envío que tu sistema controla. Si pertenecen a Bounded Contexts diferentes, conserva sus implementaciones separadas y utiliza una ACL HTTP con contratos del consumidor cuando esa sea la integración acordada.

Un fallo de consulta no equivale a «envío inexistente». Tampoco existe una transacción compartida entre la llamada HTTP y el guardado local por recibir ambas operaciones el mismo identificador de proceso. Aquí la decisión consiste en nombrar y limitar el contrato según la garantía real que puede ofrecer.

Cuándo no necesitas otro Repository genérico

Una clase GenericRepository<T> con findWhere, include, join y opciones propias del ORM puede conservar el acoplamiento bajo nombres nuevos. Empieza por las operaciones que necesitan tus consumidores: findById, guardar cambios o buscar una selección acotada de pedidos, con criterios comprensibles para el modelo.

Eso no obliga a inventar nombres comerciales para cada consulta ni a duplicar toda la API del ORM. Los detalles técnicos pueden reutilizarse dentro de Infrastructure sin convertirse en el contrato común de todos los Aggregates.

Para un catálogo CRUD pequeño, validación y un acceso a datos sencillo pueden bastar. Si no existe comportamiento de Aggregate que proteger, añadir una interfaz que solo reenvía cada llamada quizá no aporte suficiente valor. La separación debe reducir dependencias concretas, no cumplir una lista de clases.

Prompts para orientar y revisar código generado por IA

Diseñar el contrato necesario

Analiza los Use Cases de pedidos adjuntos y propón un OrderRepository sin tipos del ORM ni consultas arbitrarias del motor. Distingue inserción, recuperación, actualización y proyecciones de lectura. Especifica ausencia, duplicado y conflicto. No implementes ni añadas operaciones por defecto. Una persona validará qué información necesita el Aggregate y qué garantías espera cada consumidor.

Implementar mapeo y guardado

Implementa el contrato existente en memoria y en el adaptador relacional proporcionado. Devuelve Aggregates reconstruidos sin compartir referencias. Guarda cabecera y líneas atómicamente y condiciona la actualización a la versión leída. No añadas reglas de negocio al Repository ni instales otro ORM. Entrega pruebas de contrato, estado no guardado, versión obsoleta y fallo al escribir una línea. Una persona ejecutará las pruebas contra el motor correspondiente.

Revisar filtraciones y transacciones

Revisa este diff buscando tipos o sesiones del ORM en Domain, Aggregates incompletos, errores técnicos tratados como null, escrituras fuera de la transacción y conflictos sobrescritos sin recargar. No cambies archivos. Devuelve evidencia y un escenario observable por hallazgo. Una persona verificará conexiones, aislamiento, consultas emitidas y restricciones reales; no supongas una transacción compartida por HTTP.

Una implementación en memoria puede pasar pruebas aunque la SQL falle por restricciones o aislamiento. Utilízala para comprobar comportamiento y conserva pruebas del adaptador real para las garantías que dependen de él.

Tres ejercicios con solución razonada

1. Recuperar solo la cabecera

La IA optimiza findById() y deja de cargar líneas, pero devuelve un Order al que puede pedirse confirm(). ¿Qué revisarías?

Solución razonada: si el objeto contiene lo necesario para comprobar sus reglas. En el modelo del ejemplo, un pedido requiere líneas; una cabecera incompleta no es un Aggregate recuperado. Puede servir como DTO de listado, con otro contrato, pero no debe fingir un Order listo para modificar.

2. Reintentar después de un conflicto

El adaptador recibe cero filas actualizadas y repite el UPDATE eliminando la condición de versión. ¿Conserva el contrato?

Solución razonada: no. Sobrescribe una versión que no evaluaste. Devuelve el conflicto y decide en Application si procede recuperar, reevaluar y reintentar. Las pruebas deben comprobar que la escritura rechazada no modifica estado ni versión.

3. Pasar una transacción del ORM al modelo

Se propone order.confirm(entityManager) para que la Entity guarde sus cambios. ¿Qué dependencia introduce?

Solución razonada: acopla la operación del Domain a un mecanismo de persistencia. La Entity puede comprobar y cambiar su estado sin ese objeto; el Use Case y el Repository coordinan el guardado. Si hay varias escrituras que deben ser atómicas, establece su límite transaccional fuera del modelo y comprueba que comparten la misma unidad real.

Cómo reconocer un Repository que aporta valor

Comprueba qué objeto recupera, qué significa guardarlo y qué garantías promete ante ausencia, fallo y concurrencia. Revisa después que los tipos públicos no obliguen al consumidor a conocer conexiones, tablas o mecanismos del ORM.

Un Repository útil permite ejecutar el comportamiento del Aggregate y persistirlo mediante un contrato explícito. Esa separación se demuestra con mapeo, transacciones y pruebas del adaptador, además de una interfaz con buenos nombres.

Cargando conversación…

Sigue aprendiendo