Pular para o conteúdo

Domain Services

Para Eric Evans, quando uma operação de domínio significativa não se encaixa como responsabilidade de uma entidade ou VO — tipicamente porque coordena vários agregados ou depende de conceitos que não têm um “dono” claro — ela vira um Domain Service. É uma operação, não uma coisa: seu nome costuma ser um verbo do domínio.

Vaughn Vernon alerta que Domain Services devem ser a exceção, não a regra. Antes de criar um, pergunte se o comportamento não caberia numa entidade ou VO. O abuso de services é a principal causa do modelo anêmico.

Tipo Camada Responsabilidade
Domain Service Domínio Regra de negócio sem lar em entidade/VO; stateless
Application Service Aplicação Orquestra caso de uso, transação, coordena repositórios
Infrastructure Service Infraestrutura Detalhes técnicos: e-mail, fila, gateway

Dá um lar a lógicas como: transferência entre duas Contas (envolve dois agregados), cálculo de rota, política de precificação que consulta várias fontes. Colocá-las em uma das entidades distorceria a responsabilidade; espalhá-las na camada de aplicação as tiraria do domínio.

  1. Stateless. Não guarda estado entre chamadas.
  2. Expresso na linguagem ubíqua. ServicoDeTransferencia, não TransferHelper.
  3. Opera sobre objetos de domínio, recebendo/retornando entidades e VOs.
  4. Sem I/O direto. Persistência e chamadas externas ficam na camada de aplicação/adapters; o Domain Service contém a regra, não a orquestração.

Se a regra pode viver numa entidade ou VO, prefira colocá-la lá. Domain Service é o último recurso quando o comportamento realmente não tem dono. Usá-lo por preguiça leva ao anêmico.

Transferência entre duas contas: comportamento que envolve dois agregados, logo não pertence a nenhum deles isoladamente. A regra está no Domain Service; a orquestração/transação fica no Application Service.

package com.banco.conta.domain;
import com.loja.shared.domain.Dinheiro;
public class ServicoDeTransferencia {
public void transferir(Conta origem, Conta destino, Dinheiro valor) {
if (origem.id().equals(destino.id())) {
throw new IllegalArgumentException("Origem e destino iguais");
}
origem.debitar(valor);
destino.creditar(valor);
}
}
package com.banco.conta.domain;
import com.loja.shared.domain.Dinheiro;
public class Conta {
private final ContaId id;
private Dinheiro saldo;
public Conta(ContaId id, Dinheiro saldoInicial) {
this.id = id;
this.saldo = saldoInicial;
}
public void debitar(Dinheiro valor) {
if (valor.maiorQue(saldo)) {
throw new IllegalStateException("Saldo insuficiente");
}
this.saldo = saldo.subtrair(valor);
}
public void creditar(Dinheiro valor) {
this.saldo = saldo.somar(valor);
}
public ContaId id() {
return id;
}
public Dinheiro saldo() {
return saldo;
}
}
package com.banco.conta.application;
import com.banco.conta.domain.Conta;
import com.banco.conta.domain.ContaId;
import com.banco.conta.domain.ContaRepository;
import com.banco.conta.domain.ServicoDeTransferencia;
import com.loja.shared.domain.Dinheiro;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferenciaApplicationService {
private final ContaRepository contaRepository;
private final ServicoDeTransferencia servicoDeTransferencia;
public TransferenciaApplicationService(ContaRepository contaRepository,
ServicoDeTransferencia servicoDeTransferencia) {
this.contaRepository = contaRepository;
this.servicoDeTransferencia = servicoDeTransferencia;
}
@Transactional
public void executar(ContaId origemId, ContaId destinoId, Dinheiro valor) {
Conta origem = contaRepository.porId(origemId)
.orElseThrow(() -> new IllegalArgumentException("Conta origem inexistente"));
Conta destino = contaRepository.porId(destinoId)
.orElseThrow(() -> new IllegalArgumentException("Conta destino inexistente"));
servicoDeTransferencia.transferir(origem, destino, valor);
contaRepository.salvar(origem);
contaRepository.salvar(destino);
}
}
sequenceDiagram
    participant AS as TransferenciaApplicationService
    participant Repo as ContaRepository
    participant DS as ServicoDeTransferencia
    participant O as Conta origem
    participant D as Conta destino

    AS->>Repo: porId(origem/destino)
    Repo-->>AS: Contas
    AS->>DS: transferir(origem, destino, valor)
    DS->>O: debitar(valor)
    DS->>D: creditar(valor)
    AS->>Repo: salvar(origem/destino)
  • Modelo anêmico: mover toda a lógica para services e deixar entidades como sacos de dados.
  • Confundir camadas: colocar transação/orquestração no Domain Service, ou regra de negócio no Application Service.
  • Domain Service com estado: guardar dados entre chamadas.
  • I/O no Domain Service: chamar banco/HTTP direto, acoplando o domínio à infraestrutura.

1. Quando usar um Domain Service em vez de um método na entidade?

Resposta

Quando o comportamento não tem dono natural — tipicamente quando coordena vários agregados (ex.: transferência entre contas). Se cabe numa entidade ou VO, prefira colocar lá.

2. Qual a diferença entre Domain Service e Application Service?

Resposta

O Domain Service contém regra de negócio e é stateless, no domínio. O Application Service orquestra o caso de uso: transação, carga via Repositórios e coordenação, na camada de aplicação.

3. Por que Domain Services devem ser a exceção?

Resposta

Porque o abuso esvazia as entidades e leva ao modelo anêmico. A recomendação (Vernon) é preferir sempre comportamento nas entidades/VOs e recorrer ao service só quando não há dono.

4. Um Domain Service pode acessar o banco diretamente?

Resposta

Não. Ele contém a regra pura; I/O e persistência ficam na camada de aplicação e nos Adapters, preservando a pureza do domínio.