DDD: Domain Services vs Application Services - Organizando a lógica de negócio (Parte 6)

DDD11 min de leitura

Este é o sexto artigo da série sobre Domain-Driven Design. Nos artigos anteriores, exploramos os conceitos fundamentais do DDD, Value Objects, Entities e Agregados. Agora chegou o momento de entender uma distinção crucial na arquitetura DDD: Domain Services vs Application Services.

A confusão entre estes dois tipos de serviços é comum e pode levar a arquiteturas problemáticas. Compreender suas diferenças e responsabilidades é fundamental para criar sistemas bem organizados e testáveis.

O que são Services no DDD?

No DDD, Services (Serviços) são classes stateless que encapsulam operações que não pertencem naturalmente a nenhuma Entity ou Value Object específico. Eles representam conceitos do domínio que são expressos como verbos ou atividades, em vez de substantivos.

Como Eric Evans define:

"Às vezes, fica claro que um conceito importante do domínio não é algo natural para modelar como um objeto. O conceito que você está tentando expressar é mais uma atividade ou ação, não uma coisa."

Por que Precisamos de Services?

Nem toda lógica de negócio pertence a Entities ou Value Objects. Quando encontramos operações que:

  1. Envolvem múltiplos objetos de domínio
  2. Não têm uma "casa natural" em nenhuma Entity específica
  3. Representam conceitos importantes do domínio
  4. Precisam ser testáveis independentemente

É neste momento que Services entram em cena para evitar que Entities fiquem sobrecarregadas ou que lógica importante fique espalhada.

A Diferença Fundamental

A diferença principal entre Domain Services e Application Services pode ser resumida em uma pergunta simples:

"Este código toma decisões de negócio?"

  • Se SIM → Domain Service
  • Se NÃO → Application Service

Domain Services: A Lógica Pura do Domínio

Domain Services contêm lógica de negócio pura que é parte essencial do domínio. Eles lidam com operações que envolvem conceitos de domínio mas não pertencem a nenhuma Entity específica.

Características dos Domain Services:

  1. Contêm lógica de domínio - Tomam decisões de negócio
  2. São stateless - Não mantêm estado entre chamadas
  3. Trabalham com objetos de domínio - Entities, Value Objects, outros Domain Services
  4. Podem ser "puros" ou "impuros" - Dependendo se acessam infraestrutura

Exemplo: Calculadora de Desconto

class CalculadoraDesconto
{
    public function calcularDesconto(Cliente $cliente, Produto $produto): Desconto
    {
        // Lógica de domínio: regras de desconto baseadas no cliente
        $percentualBase = $this->obterPercentualBasePorTipoCliente($cliente->getTipo());
        
        // Aplicar bonificações por tempo de relacionamento
        $bonusTempoRelacionamento = $this->calcularBonusTempo($cliente->getTempoRelacionamento());
        
        // Verificar se produto é elegível para desconto especial
        $bonusCategoria = $this->verificarBonusCategoria($produto->getCategoria());
        
        $percentualFinal = min(
            $percentualBase + $bonusTempoRelacionamento + $bonusCategoria,
            0.5 // Limite máximo de 50%
        );
        
        return new Desconto($percentualFinal, $this->calcularJustificativa($cliente, $produto));
    }
    
    private function obterPercentualBasePorTipoCliente(TipoCliente $tipo): float
    {
        return match($tipo) {
            TipoCliente::BRONZE => 0.05,
            TipoCliente::PRATA => 0.10,
            TipoCliente::OURO => 0.15,
            TipoCliente::DIAMANTE => 0.20
        };
    }
    
    private function calcularBonusTempo(int $anosRelacionamento): float
    {
        if ($anosRelacionamento >= 10) {
            return 0.10;
        }
        if ($anosRelacionamento >= 5) {
            return 0.05;
        }
        if ($anosRelacionamento >= 2) {
            return 0.02;
        }
        
        return 0.0;
    }
    
    private function verificarBonusCategoria(CategoriaProduto $categoria): float
    {
        return match($categoria) {
            CategoriaProduto::ELETRONICOS => 0.03,
            CategoriaProduto::ROUPAS => 0.08,
            CategoriaProduto::LIVROS => 0.15,
            default => 0.0
        };
    }
    
    private function calcularJustificativa(Cliente $cliente, Produto $produto): string
    {
        return sprintf(
            "Desconto aplicado para cliente %s (%s) no produto %s da categoria %s",
            $cliente->getNome()->valor,
            $cliente->getTipo()->name,
            $produto->getNome(),
            $produto->getCategoria()->name
        );
    }
}

Application Services: Orquestração e Coordenação

Application Services são responsáveis por orquestrar operações e coordenar o fluxo de trabalho das operações de negócio. Eles atuam como uma "camada de fachada" que esconde a complexidade do domínio das camadas externas.

Características dos Application Services:

  1. Não contêm lógica de domínio - Apenas orquestram
  2. Coordenam operações - Chamam Domain Services, Aggregates, Repositories
  3. Gerenciam transações - Definem fronteiras transacionais
  4. Interface com o mundo externo - UI, APIs, mensageria
  5. Retornam DTOs - Não expõem objetos de domínio diretamente

Exemplo: Application Service para Processamento de Pedido

// Command para criar pedido
final readonly class CriarPedidoCommand
{
    public function __construct(
        public string $clienteId,
        public array $itens,
        public string $enderecoEntregaId,
        public string $metodoPagamento
    ) {}
}

// Application Service
class ProcessarPedidoApplicationService
{
    public function __construct(
        private readonly ClienteRepository $clienteRepository,
        private readonly ProdutoRepository $produtoRepository,
        private readonly PedidoRepository $pedidoRepository,
        private readonly EnderecoRepository $enderecoRepository,
        private readonly VerificadorPoliticaCredito $verificadorCredito,
        private readonly CalculadoraDesconto $calculadoraDesconto,
        private readonly NotificacaoService $notificacaoService,
        private readonly EventDispatcher $eventDispatcher
    ) {}
    
    public function criarPedido(CriarPedidoCommand $command): CriarPedidoResult
    {
        // 1. Buscar e validar dados necessários
        $cliente = $this->buscarCliente($command->clienteId);
        $endereco = $this->buscarEndereco($command->enderecoEntregaId);
        $produtos = $this->buscarProdutos($command->itens);
        
        // 2. Criar o agregado Pedido
        $pedidoId = PedidoId::gerar();
        $pedido = new Pedido($pedidoId, $cliente->getId(), $endereco);
        
        // 3. Adicionar itens ao pedido
        $valorTotal = Dinheiro::zero();
        foreach ($command->itens as $itemData) {
            $produto = $produtos[$itemData['produtoId']];
            $quantidade = $itemData['quantidade'];
            
            // Verificar disponibilidade em estoque
            if (!$produto->temEstoqueSuficiente($quantidade)) {
                throw new EstoqueInsuficienteException($produto->getId(), $quantidade);
            }
            
            $pedido->adicionarItem($produto->getId(), $quantidade, $produto->getPreco());
            $valorTotal = $valorTotal->somar($produto->getPreco()->multiplicar($quantidade));
        }
        
        // 4. Aplicar desconto usando Domain Service
        $desconto = $this->calculadoraDesconto->calcularDesconto($cliente, $pedido);
        if ($desconto->getPercentual() > 0) {
            $pedido->aplicarDesconto($desconto);
            $valorTotal = $valorTotal->aplicarDesconto($desconto->getPercentual());
        }
        
        // 5. Verificar política de crédito se for pagamento a prazo
        if ($command->metodoPagamento === 'CREDITO') {
            $resultadoCredito = $this->verificadorCredito->podeReceberCredito($cliente, $valorTotal);
            if (!$resultadoCredito->foiAprovado()) {
                throw new CreditoNegadoException($resultadoCredito->getMotivo());
            }
        }
        
        // 6. Confirmar pedido
        $pedido->confirmar();
        
        // 7. Reduzir estoque dos produtos
        foreach ($command->itens as $itemData) {
            $produto = $produtos[$itemData['produtoId']];
            $produto->reduzirEstoque($itemData['quantidade']);
            $this->produtoRepository->salvar($produto);
        }
        
        // 8. Salvar pedido
        $this->pedidoRepository->salvar($pedido);
        
        // 9. Enviar notificações
        $this->notificacaoService->enviarConfirmacaoPedido($cliente, $pedido);
        
        // 10. Publicar eventos de domínio
        $this->eventDispatcher->dispatch(new PedidoCriadoEvent($pedido->getId(), $valorTotal));
        
        return new CriarPedidoResult(
            $pedido->getId()->valor,
            $valorTotal->valor,
            $desconto->getPercentual(),
            "Pedido criado com sucesso"
        );
    }
    
    private function buscarCliente(string $clienteId): Cliente
    {
        $cliente = $this->clienteRepository->buscarPorId(new ClienteId($clienteId));
        if (!$cliente) {
            throw new ClienteNaoEncontradoException($clienteId);
        }
        return $cliente;
    }
    
    private function buscarEndereco(string $enderecoId): Endereco
    {
        $endereco = $this->enderecoRepository->buscarPorId(new EnderecoId($enderecoId));
        if (!$endereco) {
            throw new EnderecoNaoEncontradoException($enderecoId);
        }
        return $endereco;
    }
    
    private function buscarProdutos(array $itens): array
    {
        $produtos = [];
        foreach ($itens as $item) {
            $produto = $this->produtoRepository->buscarPorId(new ProdutoId($item['produtoId']));
            if (!$produto) {
                throw new ProdutoNaoEncontradoException($item['produtoId']);
            }
            $produtos[$item['produtoId']] = $produto;
        }
        return $produtos;
    }
}

// DTO de resposta
final readonly class CriarPedidoResult
{
    public function __construct(
        public string $pedidoId,
        public float $valorTotal,
        public float $percentualDesconto,
        public string $mensagem
    ) {}
}

Domain Services Puros vs Impuros

Vladimir Khorikov faz uma distinção importante entre Domain Services "puros" e "impuros":

Domain Services Puros

Características:

  • Trabalham apenas com objetos de domínio
  • Não acessam infraestrutura externa
  • São completamente testáveis sem mocks
  • Podem ser injetados em Entities sem quebrar isolamento
// Domain Service PURO
class ValidadorCpf
{
    public function isValido(Cpf $cpf): bool
    {
        $numeros = preg_replace('/[^0-9]/', '', $cpf->valor);
        
        if (strlen($numeros) !== 11) {
            return false;
        }
        
        // Verifica sequências inválidas
        if (preg_match('/(\d)\1{10}/', $numeros)) {
            return false;
        }
        
        // Valida dígitos verificadores
        return $this->validarDigitosVerificadores($numeros);
    }
    
    private function validarDigitosVerificadores(string $numeros): bool
    {
        // Lógica pura de validação de CPF
        $soma = 0;
        for ($i = 0; $i < 9; $i++) {
            $soma += intval($numeros[$i]) * (10 - $i);
        }
        
        $primeiroDigito = 11 - ($soma % 11);
        if ($primeiroDigito >= 10) {
            $primeiroDigito = 0;
        }
        
        if (intval($numeros[9]) !== $primeiroDigito) {
            return false;
        }
        
        $soma = 0;
        for ($i = 0; $i < 10; $i++) {
            $soma += intval($numeros[$i]) * (11 - $i);
        }
        
        $segundoDigito = 11 - ($soma % 11);
        if ($segundoDigito >= 10) {
            $segundoDigito = 0;
        }
        
        return intval($numeros[10]) === $segundoDigito;
    }
}

Domain Services Impuros

Características:

  • Precisam acessar sistemas externos
  • Requerem mocks para testes
  • Não devem ser injetados em Entities
  • Mantêm a lógica de domínio mesmo dependendo de infraestrutura
// Domain Service IMPURO
class VerificadorPoliticaCredito
{
    public function __construct(
        private readonly HistoricoTransacoesRepository $historicoRepository,
        private readonly ServicoConsultaCredito $consultaCredito
    ) {}
    
    public function podeReceberCredito(Cliente $cliente, Dinheiro $valorSolicitado): ResultadoAnaliseCredito
    {
        // 1. Verificar limite de crédito baseado no perfil do cliente
        $limitePerfil = $this->calcularLimitePorPerfil($cliente);
        if ($valorSolicitado->isMaiorQue($limitePerfil)) {
            return ResultadoAnaliseCredito::negado("Valor excede limite por perfil de cliente");
        }
        
        // 2. Consultar histórico de transações do cliente
        $historico = $this->historicoRepository->buscarUltimasTransacoes($cliente->getId(), 12);
        $scoreHistorico = $this->calcularScoreHistorico($historico);
        
        if ($scoreHistorico < 0.6) {
            return ResultadoAnaliseCredito::negado("Score de histórico insuficiente");
        }
        
        // 3. Consultar órgãos de proteção ao crédito
        $consultaExterna = $this->consultaCredito->consultarCpf($cliente->getCpf());
        if ($consultaExterna->temRestricoes()) {
            return ResultadoAnaliseCredito::negado("Cliente com restrições em órgãos de proteção");
        }
        
        // 4. Aplicar algoritmo de decisão final
        $limiteAprovado = $this->calcularLimiteFinal($limitePerfil, $scoreHistorico, $consultaExterna);
        
        if ($valorSolicitado->isMenorOuIgual($limiteAprovado)) {
            return ResultadoAnaliseCredito::aprovado($limiteAprovado, $scoreHistorico);
        }
        
        return ResultadoAnaliseCredito::aprovadoComReducao($limiteAprovado, "Valor reduzido conforme análise de risco");
    }
    
    private function calcularLimitePorPerfil(Cliente $cliente): Dinheiro
    {
        $baseLimit = match($cliente->getTipo()) {
            TipoCliente::BRONZE => 1000,
            TipoCliente::PRATA => 5000,
            TipoCliente::OURO => 15000,
            TipoCliente::DIAMANTE => 50000
        };
        
        // Ajuste por tempo de relacionamento
        $multiplicador = 1 + ($cliente->getTempoRelacionamento() * 0.1);
        
        return Dinheiro::reais($baseLimit * $multiplicador);
    }
    
    private function calcularScoreHistorico(array $transacoes): float
    {
        if (empty($transacoes)) {
            return 0.5; // Score neutro para clientes sem histórico
        }
        
        $totalTransacoes = count($transacoes);
        $transacoesPontuais = 0;
        $valorMedioTransacoes = 0;
        
        foreach ($transacoes as $transacao) {
            if ($transacao->foiLiquidadaNoPrazo()) {
                $transacoesPontuais++;
            }
            $valorMedioTransacoes += $transacao->getValor()->valor;
        }
        
        $scorePontualidade = $transacoesPontuais / $totalTransacoes;
        $scoreVolume = min($valorMedioTransacoes / $totalTransacoes / 1000, 1.0);
        
        return ($scorePontualidade * 0.7) + ($scoreVolume * 0.3);
    }
    
    private function calcularLimiteFinal(
        Dinheiro $limitePerfil, 
        float $scoreHistorico, 
        ConsultaCreditoResult $consultaExterna
    ): Dinheiro {
        $fatorScore = 0.5 + ($scoreHistorico * 0.5); // Entre 0.5 e 1.0
        $fatorConsulta = $consultaExterna->getScore() / 100; // Normalizar para 0-1
        
        $fatorFinal = ($fatorScore + $fatorConsulta) / 2;
        
        return $limitePerfil->multiplicar($fatorFinal);
    }
}

Testando Domain Services vs Application Services

A testabilidade é diferente entre os dois tipos de serviços:

Testando Domain Services (Puros)

class CalculadoraDescontoTest extends TestCase
{
    public function test_cliente_ouro_com_5_anos_deve_receber_20_por_cento_desconto(): void
    {
        // Arrange
        $calculadora = new CalculadoraDesconto();
        $cliente = new Cliente(
            ClienteId::gerar(),
            new Nome("João Silva"),
            TipoCliente::OURO,
            5 // anos de relacionamento
        );
        $produto = new Produto(
            ProdutoId::gerar(),
            "Smartphone",
            CategoriaProduto::ELETRONICOS,
            Dinheiro::reais(1000)
        );
        
        // Act
        $desconto = $calculadora->calcularDesconto($cliente, $produto);
        
        // Assert
        $this->assertEquals(0.23, $desconto->getPercentual()); // 15% + 5% + 3%
    }
}

Testando Application Services

class ProcessarPedidoApplicationServiceTest extends TestCase
{
    public function test_deve_criar_pedido_com_desconto_aplicado(): void
    {
        // Arrange
        $clienteRepository = $this->createMock(ClienteRepository::class);
        $produtoRepository = $this->createMock(ProdutoRepository::class);
        $pedidoRepository = $this->createMock(PedidoRepository::class);
        $calculadoraDesconto = $this->createMock(CalculadoraDesconto::class);
        
        $service = new ProcessarPedidoApplicationService(
            $clienteRepository,
            $produtoRepository,
            $pedidoRepository,
            // outros mocks...
            $calculadoraDesconto
            // outros mocks...
        );
        
        $cliente = new Cliente(/* ... */);
        $produto = new Produto(/* ... */);
        $desconto = new Desconto(0.10, "Desconto cliente ouro");
        
        // Configurar mocks
        $clienteRepository->method('buscarPorId')->willReturn($cliente);
        $produtoRepository->method('buscarPorId')->willReturn($produto);
        $calculadoraDesconto->method('calcularDesconto')->willReturn($desconto);
        
        $command = new CriarPedidoCommand(
            clienteId: $cliente->getId()->valor,
            itens: [['produtoId' => $produto->getId()->valor, 'quantidade' => 2]],
            enderecoEntregaId: 'endereco-123',
            metodoPagamento: 'CARTAO'
        );
        
        // Act
        $resultado = $service->criarPedido($command);
        
        // Assert
        $this->assertNotEmpty($resultado->pedidoId);
        $this->assertEquals(0.10, $resultado->percentualDesconto);
    }
}

Anti-Patterns Comuns

1. Application Service com Lógica de Domínio

// ❌ ERRADO: Application Service tomando decisões de negócio
class ProcessarPedidoApplicationService
{
    public function criarPedido(CriarPedidoCommand $command): void
    {
        $pedido = $this->pedidoRepository->buscarPorId($command->pedidoId);
        
        // ❌ LÓGICA DE DOMÍNIO no Application Service
        $desconto = 0;
        if ($pedido->getCliente()->getTipo() === TipoCliente::OURO) {
            $desconto = 0.15;
        } elseif ($pedido->getCliente()->getTipo() === TipoCliente::PRATA) {
            $desconto = 0.10;
        }
        
        $pedido->aplicarDesconto($desconto);
    }
}

Solução:

// ✓ CORRETO: Delegar decisão para Domain Service
class ProcessarPedidoApplicationService
{
    public function criarPedido(CriarPedidoCommand $command): void
    {
        $pedido = $this->pedidoRepository->buscarPorId($command->pedidoId);
        
        // ✓ Delega a decisão para o Domain Service
        $desconto = $this->calculadoraDesconto->calcularDesconto(
            $pedido->getCliente(), 
            $pedido
        );
        
        $pedido->aplicarDesconto($desconto);
    }
}

2. Domain Service Fazendo Orquestração

// ❌ ERRADO: Domain Service fazendo orquestração
class ProcessadorPedido // Domain Service?
{
    public function processar(Pedido $pedido): void
    {
        // ❌ Domain Service não deveria salvar no banco
        $this->pedidoRepository->salvar($pedido);
        
        // ❌ Domain Service não deveria enviar emails
        $this->emailService->enviarConfirmacao($pedido);
        
        // ❌ Domain Service não deveria chamar APIs externas
        $this->pagamentoApi->processarPagamento($pedido);
    }
}

Conclusão

A distinção entre Domain Services e Application Services é fundamental para uma arquitetura DDD bem organizada:

Domain Services:

  • Contêm lógica de negócio que não pertence a nenhuma Entity específica
  • Tomam decisões baseadas em regras do domínio
  • São testáveis com objetos de domínio
  • Representam conceitos importantes do domínio

Application Services:

  • Orquestram operações sem tomar decisões de negócio
  • Coordenam Domain Services, Aggregates e Infrastructure
  • Gerenciam transações e efeitos colaterais
  • Servem como fachada para casos de uso

Regra de Ouro:

Se o código precisa tomar uma decisão de negócio, é Domain Service. Se apenas coordena operações, é Application Service.

Quando implementados corretamente, estes serviços resultam em uma arquitetura mais limpa, testável e alinhada com os princípios do DDD, facilitando a manutenção e evolução do sistema.

No próximo artigo da série, exploraremos Repositories - Abstraindo o acesso aos dados, entendendo como implementar o padrão Repository de forma eficaz no DDD.


Referências

  • Evans, Eric. "Domain-Driven Design: Tackling Complexity in the Heart of Software". Addison-Wesley, 2003.
  • Khorikov, Vladimir. "Domain services vs Application services". Enterprise Craftsmanship, 2016.
  • Vernon, Vaughn. "Implementing Domain-Driven Design". Addison-Wesley, 2013.
  • Bueno, Carlos. "O que é Domain Services? Como usar?". Medium, 2020.
  • Pantiushin, Sergei. "Implementing DDD in PHP". Medium, 2024.
  • Santos, Calebe. "Understanding the Application-Services Patterns in Domain-Driven Design". Kranio, 2025.
  • Young, Greg. "CQRS Documents". goodlydocs.io
  • Stemmler, Khalil. "Domain Services vs Application Services". khalilstemmler.com
  • Gorodinski, Lev. "Services in Domain-Driven Design". Lev Gorodinski's Blog
  • Fowler, Martin. "Service Layer Pattern". martinfowler.com