Application layering

@ApplicationService and @DomainService are provided by spring-ddd-annotations and define the transaction conventions for the application and domain layers. Both are independent of the persistence strategy — they work whether you use event sourcing, JPA aggregates, or a combination.

Application layering: a caller invokes an @ApplicationService (REQUIRED) that owns the transaction boundary; within it an @DomainService (MANDATORY) loads and saves aggregates and repositories
Figure 1. How the layers share a transaction

Application service

@ApplicationService marks a class as the entry point for a use case. It owns the transaction boundary for application-layer work: reading state, building commands, and interpreting results. The default propagation is REQUIRED — it joins an existing transaction if one is active, otherwise starts a new one.

import de.dwittkoetter.ddd.annotation.ApplicationService
import de.dwittkoetter.ddd.cqrs.command.CommandGateway

@ApplicationService
class AccountApplicationService(private val commandGateway: CommandGateway) {

    fun openAccount(accountId: AccountId, initialBalance: Money) {
        commandGateway.send(OpenAccount(accountId, initialBalance))
    }

    fun deposit(accountId: AccountId, amount: Money) {
        commandGateway.send(DepositMoney(accountId, amount))
    }
}
A bean that does nothing but dispatch a single command rarely needs @ApplicationService — each command handler already runs in its own transaction. Reach for it when a use case reads state, coordinates more than one command, or interprets results within a single transaction boundary.

Domain service

@DomainService marks a class that encapsulates domain logic spanning more than one aggregate. The default propagation is MANDATORY — a domain service must be called within an existing transaction and never starts its own.

import de.dwittkoetter.ddd.annotation.DomainService

@DomainService
class TransferService(
    private val accounts: BankAccountRepository,
) {

    fun transfer(sourceId: AccountId, targetId: AccountId, amount: Money) {
        val source = accounts.findById(sourceId).orElseThrow()
        val target = accounts.findById(targetId).orElseThrow()
        source.withdraw(amount)
        target.deposit(amount)
        accounts.save(source)
        accounts.save(target)
    }
}

The caller — typically an @ApplicationService — is responsible for opening the transaction before invoking a domain service.