DDD: Domain Services vs Application Services - Organizando a lógica de negócio (Parte 6)
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:
- Envolvem múltiplos objetos de domínio
- Não têm uma "casa natural" em nenhuma Entity específica
- Representam conceitos importantes do domínio
- 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:
- Contêm lógica de domínio - Tomam decisões de negócio
- São stateless - Não mantêm estado entre chamadas
- Trabalham com objetos de domínio - Entities, Value Objects, outros Domain Services
- 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:
- Não contêm lógica de domínio - Apenas orquestram
- Coordenam operações - Chamam Domain Services, Aggregates, Repositories
- Gerenciam transações - Definem fronteiras transacionais
- Interface com o mundo externo - UI, APIs, mensageria
- 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