Domain Services e Application Services

1. Fundamentos: O Papel dos Services na Arquitetura DDD

No Domain-Driven Design (DDD), os Services representam um dos blocos fundamentais da camada de domínio, ao lado de Entities e Value Objects. Enquanto Entities encapsulam estado e comportamento, e Value Objects representam conceitos imutáveis com igualdade baseada em atributos, os Services surgem para operações que não pertencem naturalmente a nenhum desses componentes.

1.1. Diferença entre Services e componentes de domínio

Entities possuem identidade e ciclo de vida. Value Objects são imutáveis e descritivos. Services, por outro lado, são stateless e representam comportamento puro. Um Service é necessário quando uma operação envolve múltiplos agregados ou não se encaixa como responsabilidade de uma única entidade.

1.2. Quando um Service é necessário

Um Service é indicado quando:
- A operação envolve lógica que não pertence a uma única Entity ou Value Object
- A operação requer coordenação entre múltiplos agregados
- A operação é um cálculo complexo sem estado natural

1.3. Relação com o princípio de responsabilidade única

Services ajudam a manter o princípio de responsabilidade única (SRP) ao extrair operações que, se colocadas em Entities, violariam a coesão do domínio. Cada Service deve ter uma única responsabilidade bem definida.

2. Domain Services: Lógica de Negócio Pura

2.1. Características

Domain Services são componentes stateless que executam operações atômicas do domínio. Eles operam exclusivamente com conceitos do domínio e não têm dependências de infraestrutura.

// Domain Service puro - sem dependências externas
class TransferDomainService {
    fun execute(transfer: Transfer, accounts: List<Account>): TransferResult {
        val sourceAccount = accounts.find { it.id == transfer.sourceId }
            ?: throw AccountNotFoundException(transfer.sourceId)
        val targetAccount = accounts.find { it.id == transfer.targetId }
            ?: throw AccountNotFoundException(transfer.targetId)

        // Lógica de negócio pura
        if (sourceAccount.balance < transfer.amount) {
            return TransferResult.insufficientFunds()
        }
        if (transfer.amount > sourceAccount.dailyLimit) {
            return TransferResult.exceedsDailyLimit()
        }

        sourceAccount.debit(transfer.amount)
        targetAccount.credit(transfer.amount)

        return TransferResult.success(transfer.id)
    }
}

2.2. Exemplos típicos

  • Cálculos financeiros complexos (juros, impostos)
  • Validações que envolvem múltiplos agregados
  • Regras de elegibilidade que cruzam diferentes entidades

2.3. Interação com Repositories e Aggregates

Domain Services recebem Aggregates como parâmetros, mas nunca acessam Repositories diretamente. A busca por agregados é responsabilidade de quem chama o Service.

class EligibilityDomainService {
    fun checkEligibility(customer: Customer, loanRequest: LoanRequest): EligibilityResult {
        if (customer.creditScore < loanRequest.minimumScore) {
            return EligibilityResult.denied("Credit score below minimum")
        }
        if (customer.debtRatio + loanRequest.monthlyPayment > customer.income * 0.4) {
            return EligibilityResult.denied("Debt ratio exceeds 40%")
        }
        return EligibilityResult.approved()
    }
}

3. Application Services: Orquestração e Casos de Uso

3.1. Responsabilidades

Application Services são a fachada da aplicação. Eles orquestram o fluxo completo de um caso de uso, coordenando:
- Transações de banco de dados
- Segurança e autorização
- Chamadas a Domain Services
- Persistência via Repositories
- Envio de notificações e eventos

3.2. Diferença crucial

Application Services não contêm lógica de negócio. Eles são thin controllers que delegam para o domínio. Se um Application Service começa a conter regras de negócio, você está criando um domínio anêmico.

3.3. Ciclo de vida típico

class TransferApplicationService(
    private val accountRepository: AccountRepository,
    private val transferDomainService: TransferDomainService,
    private val unitOfWork: UnitOfWork
) {
    fun execute(request: TransferRequest): TransferResponse {
        // 1. Validação de entrada (DTO)
        val transfer = Transfer(
            id = TransferId.generate(),
            sourceId = AccountId(request.sourceAccount),
            targetId = AccountId(request.targetAccount),
            amount = Money(request.amount),
            timestamp = Clock.now()
        )

        // 2. Busca de agregados
        val accounts = accountRepository.findByIds(transfer.sourceId, transfer.targetId)

        // 3. Delegação ao domínio
        val result = transferDomainService.execute(transfer, accounts)

        // 4. Persistência e transação
        if (result.isSuccess) {
            unitOfWork.begin()
            accountRepository.saveAll(accounts)
            unitOfWork.commit()
        }

        // 5. Retorno do resultado
        return TransferResponse(result)
    }
}

4. Comparação Detalhada: Domain vs Application Services

4.1. Escopo

Aspecto Domain Service Application Service
Foco Regras de negócio Fluxo da aplicação
Conhecimento Apenas domínio Domínio + infraestrutura
Reutilização Entre aplicações Específico da aplicação

4.2. Dependências

Domain Services dependem apenas de:
- Entities e Value Objects
- Outros Domain Services (via injeção)

Application Services dependem de:
- Domain Services
- Repositories
- Serviços de infraestrutura (email, filas, APIs)
- DTOs e mappers

4.3. Testabilidade

// Teste unitário para Domain Service - sem mocks
@Test
fun `should reject transfer when insufficient funds`() {
    val service = TransferDomainService()
    val transfer = Transfer(sourceId = "1", targetId = "2", amount = 1000)
    val accounts = listOf(
        Account("1", balance = 500),
        Account("2", balance = 200)
    )

    val result = service.execute(transfer, accounts)

    assertThat(result.isFailure).isTrue()
    assertThat(result.error).isEqualTo("Insufficient funds")
}
// Teste de integração para Application Service
@Test
fun `should complete transfer end-to-end`() {
    val appService = TransferApplicationService(
        accountRepository = postgresAccountRepository,
        transferDomainService = TransferDomainService(),
        unitOfWork = JpaUnitOfWork()
    )

    val response = appService.execute(TransferRequest("1", "2", 300))

    assertThat(response.status).isEqualTo("SUCCESS")
    assertThat(accountRepository.findById("1").balance).isEqualTo(200)
}

5. Padrões de Implementação e Boas Práticas

5.1. Injeção de dependências

Sempre use injeção de dependências (DI) para Services. Domain Services devem receber apenas dependências do domínio, enquanto Application Services podem receber dependências de infraestrutura.

5.2. Nomenclatura e organização

src/
  domain/
    services/
      TransferDomainService.kt
      EligibilityDomainService.kt
  application/
    services/
      TransferApplicationService.kt
      LoanApplicationService.kt

5.3. Tratamento de erros

Domain Services lançam exceções de domínio (semânticas do negócio). Application Services convertem exceções técnicas em respostas amigáveis.

// Domain Service - exceção de domínio
class InsufficientFundsException(accountId: String) : 
    DomainException("Account $accountId has insufficient funds")

// Application Service - tratamento
try {
    transferDomainService.execute(transfer, accounts)
} catch (e: InsufficientFundsException) {
    return TransferResponse.error("Saldo insuficiente")
} catch (e: DatabaseException) {
    return TransferResponse.error("Erro interno, tente novamente")
}

6. Integração com Outros Padrões DDD

6.1. Domain Services e Domain Events

Domain Services podem disparar Domain Events após operações bem-sucedidas:

class TransferDomainService(private val eventPublisher: DomainEventPublisher) {
    fun execute(transfer: Transfer, accounts: List<Account>): TransferResult {
        // ... lógica de transferência ...
        eventPublisher.publish(TransferCompletedEvent(transfer.id, transfer.amount))
        return TransferResult.success(transfer.id)
    }
}

6.2. Application Services e Bounded Contexts

Application Services são o ponto de integração entre Bounded Contexts, usando serviços de aplicação de outros contextos:

class OrderApplicationService(
    private val paymentContextService: PaymentApplicationService,
    private val inventoryContextService: InventoryApplicationService
) {
    fun placeOrder(order: Order): OrderResult {
        val paymentResult = paymentContextService.processPayment(order.payment)
        val inventoryResult = inventoryContextService.reserveItems(order.items)
        return OrderResult(paymentResult, inventoryResult)
    }
}

6.3. Services como fachada

Application Services atuam como fachada, escondendo a complexidade do domínio para camadas superiores (controllers, APIs).

7. Anti-padrões e Armadilhas Comuns

7.1. Lógica de negócio em Application Services

// ERRADO: Application Service com lógica de negócio
class TransferApplicationService {
    fun execute(request: TransferRequest) {
        if (request.amount > 10000) {  // ← Regra de negócio aqui!
            throw BusinessException("Valor excede limite")
        }
        // ...
    }
}

// CERTO: Lógica de negócio no Domain Service
class TransferDomainService {
    fun execute(transfer: Transfer, accounts: List<Account>) {
        if (transfer.amount > transfer.dailyLimit) {
            throw BusinessException("Valor excede limite")
        }
        // ...
    }
}

7.2. Domain Services com dependências de infraestrutura

// ERRADO: Domain Service acessando banco de dados
class TransferDomainService(private val accountRepository: AccountRepository) {
    fun execute(transfer: Transfer) {
        val accounts = accountRepository.findAll()  // ← Dependência de infraestrutura!
        // ...
    }
}

7.3. Services inchados

Sinais de que um Service precisa ser extraído:
- Mais de 5 métodos públicos
- Mais de 3 dependências injetadas
- Métodos com mais de 30 linhas
- Responsabilidades misturadas (ex: calcular, validar, notificar)

8. Exemplo Prático: Sistema de Transferência Bancária

8.1. Domain Service: TransferDomainService

class TransferDomainService {
    fun execute(
        transfer: Transfer,
        sourceAccount: Account,
        targetAccount: Account
    ): TransferResult {
        // Validações de negócio
        if (sourceAccount.isFrozen) {
            return TransferResult.failure("Source account is frozen")
        }
        if (targetAccount.isClosed) {
            return TransferResult.failure("Target account is closed")
        }
        if (sourceAccount.balance < transfer.amount) {
            return TransferResult.failure("Insufficient funds")
        }
        if (transfer.amount > sourceAccount.dailyWithdrawalLimit) {
            return TransferResult.failure("Exceeds daily withdrawal limit")
        }

        // Operações atômicas do domínio
        sourceAccount.debit(transfer.amount)
        targetAccount.credit(transfer.amount)

        return TransferResult.success(
            transactionId = TransactionId.generate(),
            newSourceBalance = sourceAccount.balance,
            newTargetBalance = targetAccount.balance
        )
    }
}

8.2. Application Service: TransferApplicationService

class TransferApplicationService(
    private val accountRepository: AccountRepository,
    private val transferDomainService: TransferDomainService,
    private val transactionRepository: TransactionRepository,
    private val eventPublisher: EventPublisher,
    private val unitOfWork: UnitOfWork
) {
    fun execute(request: TransferRequest): TransferResponse {
        // 1. Validação sintática do request
        validateRequest(request)

        // 2. Mapeamento para objetos de domínio
        val transfer = Transfer(
            amount = Money(request.amount),
            description = request.description
        )

        // 3. Busca de agregados
        val sourceAccount = accountRepository.findById(request.sourceAccountId)
            ?: return TransferResponse.notFound("Source account not found")
        val targetAccount = accountRepository.findById(request.targetAccountId)
            ?: return TransferResponse.notFound("Target account not found")

        // 4. Delegação ao Domain Service
        val result = transferDomainService.execute(transfer, sourceAccount, targetAccount)

        // 5. Persistência em transação
        if (result.isSuccess) {
            unitOfWork.begin()
            try {
                accountRepository.save(sourceAccount)
                accountRepository.save(targetAccount)
                transactionRepository.save(Transaction.from(result))
                unitOfWork.commit()

                // 6. Eventos pós-transação
                eventPublisher.publish(TransferCompletedEvent(result.transactionId))

                return TransferResponse.success(result)
            } catch (e: Exception) {
                unitOfWork.rollback()
                return TransferResponse.error("Transaction failed, please retry")
            }
        }

        return TransferResponse.failure(result.error)
    }

    private fun validateRequest(request: TransferRequest) {
        require(request.amount > 0) { "Amount must be positive" }
        require(request.sourceAccountId != request.targetAccountId) { 
            "Cannot transfer to same account" 
        }
    }
}

8.3. Comparação de código: o que muda entre os dois tipos

Aspecto TransferDomainService TransferApplicationService
Dependências Nenhuma (domínio puro) Repository, EventPublisher, UnitOfWork
Lógica Regras de negócio Coordenação, transação
Testes Unitários (sem mocks) Integração (com mocks/infra)
Responsabilidade Validar e executar transferência Orquestrar fluxo completo
Retorno TransferResult (domínio) TransferResponse (DTO)

Referências