Tabla de Contenidos

04.1 - Modelos y servicios de dominio

La capa domain contiene las reglas y conceptos propios del negocio. Debe expresarlos sin depender de mecanismos de presentación o persistencia.

1. Modelos anémicos y enriquecidos

Un modelo anémico representa datos y deja las reglas y operaciones en otros componentes. Un modelo enriquecido combina estado y comportamiento: protege sus invariantes y ofrece operaciones que expresan acciones del negocio.

El mismo concepto de pedido puede expresarse como datos sin comportamiento o como un objeto que protege sus propias reglas:

// Modelo anémico: estructura de datos
public record OrderData(long id, OrderStatus status) {}

// Modelo enriquecido: estado y comportamiento
public class Order {
    private OrderStatus status;

    public void cancel() {
        if (status != OrderStatus.PENDING) {
            throw new OrderCannotBeCancelledException();
        }
        status = OrderStatus.CANCELLED;
    }
}

Estilo Ventajas Costes y límites
Anémico Estructura sencilla; puede compartirse como datos entre componentes El comportamiento queda fuera del objeto; compartir el mismo tipo entre capas acopla sus contratos y facilita que las reglas se dispersen o dupliquen
Enriquecido Reglas e invariantes junto al estado que protegen; aprovecha la encapsulación de la POO Para separar el modelo interno de los contratos externos se necesitan DTO y mapeos; un cambio puede afectar a varias representaciones

Los estilos pueden combinarse. La elección depende de la complejidad del dominio y de cómo mantener explícitos los límites sin duplicar reglas.

2. Servicios de dominio

Un servicio de dominio expresa una operación que combina varios modelos o una regla que no pertenece naturalmente a uno solo. Si una regla protege la invariante de un modelo concreto, debe vivir en ese modelo.

Con modelos anémicos, las operaciones que combinan datos y reglas deben residir fuera de ellos. Los servicios reúnen ese comportamiento; si hay varios flujos de aplicación, los casos de uso pueden coordinarlos y reutilizar esos servicios sin duplicar el flujo. No es una obligación derivada del estilo: se separan cuando hay una responsabilidad de aplicación que coordinar.

Con modelos enriquecidos, las reglas que protegen el estado de un objeto pueden vivir en el propio modelo. Los servicios siguen siendo útiles para operaciones que involucran varios modelos o políticas que no pertenecen a uno solo.

Cuando un contrato de servicio cruza límites entre capas o módulos, puede expresarse con DTO para transportar únicamente los datos necesarios, sin exponer directamente los modelos enriquecidos. En Java, los record permiten declarar estos contratos de forma concisa:

public record CancelOrderDto(long orderId) {}
public record OrderSummaryDto(long id, OrderStatus status) {}

public interface OrderService {
    OrderSummaryDto cancel(CancelOrderDto request);
}

public class OrderServiceImpl implements OrderService {
    private final OrderRepository orders;
    private final OrderMapper mapper;

    public OrderServiceImpl(OrderRepository orders, OrderMapper mapper) {
        this.orders = orders;
        this.mapper = mapper;
    }

    @Override
    public OrderSummaryDto cancel(CancelOrderDto request) {
        Order order = orders.findById(request.orderId())
                .orElseThrow(OrderNotFoundException::new);
        order.cancel();
        return mapper.toSummary(order);
    }
}

El servicio recibe y devuelve DTO; cuando necesita convertir entre el modelo enriquecido y el formato de salida, MapStruct puede generar el código repetitivo del mapeo:

@Mapper
public interface OrderMapper {
    OrderSummaryDto toSummary(Order order);
}

El mapeador transforma datos; no aplica reglas de negocio. Su construcción e inyección se resuelven en la composición de la aplicación, sin añadir dependencias de Spring al dominio.

Un servicio puede colaborar con otros servicios de su módulo. Si el servicio requerido pertenece a otro módulo, la llamada se realiza a través de la API pública de ese módulo y su contrato DTO. La colaboración corresponde al servicio cuando es parte estable de su comportamiento; si depende de un flujo concreto, la coordina el caso de uso. Esta distinción se desarrolla en el tema 04.2.

3. Repositorios como abstracción

El repositorio expresa qué operaciones necesita el dominio para recuperar y guardar modelos. La interfaz se define en domain y su implementación de acceso a datos queda fuera, en la capa de persistencia.

La interfaz declara las operaciones desde la perspectiva del dominio, usando el modelo enriquecido:

public interface OrderRepository {
    Optional<Order> findById(long id);
    void save(Order order);
}

domainpersistenceOrderRepositoryOrderServiceImplOrderOrderRepositoryImplBase de datosutilizaaplica reglasimplementa

Esta separación aplica inversión de dependencias: el dominio declara la abstracción que necesita; la persistencia la implementa.

4. Pruebas de servicios y modelos

Una prueba de servicio debe comprobar el comportamiento del dominio, no volver a probar la persistencia. La implementación real del repositorio y su integración con el almacenamiento se comprueban en pruebas de persistencia.

@ExtendWith(MockitoExtension.class)
class OrderServiceImplTest {
    @Mock OrderRepository orders;
    @Mock OrderMapper mapper;
    @InjectMocks OrderServiceImpl service;

    @Test
    void cancelsPendingOrder() {
        Order order = new Order(OrderStatus.PENDING);
        OrderSummaryDto summary = new OrderSummaryDto(42L, OrderStatus.CANCELLED);
        when(orders.findById(42L)).thenReturn(Optional.of(order));
        when(mapper.toSummary(order)).thenReturn(summary);

        OrderSummaryDto result = service.cancel(new CancelOrderDto(42L));

        assertEquals(OrderStatus.CANCELLED, order.getStatus());
        assertEquals(summary, result);
        verify(orders).findById(42L);
        verify(mapper).toSummary(order);
    }
}

El repositorio es un mock: la prueba verifica el servicio y la regla del modelo sin iniciar una base de datos. Las operaciones reales de almacenamiento se comprueban en pruebas de persistencia.

Los modelos ricos pueden probarse como objetos ordinarios. Aísla las dependencias externas de los servicios y reserva las pruebas de base de datos para la capa de persistencia.