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
- Domain Services in Domain-Driven Design (Martin Fowler) — Artigo clássico explicando o conceito de Domain Services e quando utilizá-los na modelagem de domínio.
- Application Services vs Domain Services (DDD Community) — Fórum oficial da comunidade DDD com discussões detalhadas sobre a separação entre Application e Domain Services.
- Implementing Domain Services (Microsoft .NET Architecture Guide) — Guia oficial da Microsoft sobre implementação de Domain Services em arquiteturas baseadas em DDD.
- Domain Services and Application Services in Clean Architecture (Vaughn Vernon) — Post do autor de "Implementing Domain-Driven Design" sobre a integração de Services com Clean Architecture.
- How to Design Application Services (Herberto Graça) — Tutorial prático comparando implementações de Domain e Application Services com exemplos em TypeScript.
- DDD: Domain Services vs Application Services (Khalil Stemmler) — Artigo detalhado com exemplos em TypeScript sobre a diferença entre os dois tipos de Services no contexto DDD.