// Domain-Driven Design · 09
Domain Service
Domain logic that fits no single entity, in a stateless service.
How it fits together
- Transfer endpoint uses / calls Application service (1 POST)
- Application service uses / calls Account repository (2 load A, B)
- Application service uses / calls FundsTransfer (3 Transfer())
- FundsTransfer uses / calls Account A (4 Debit)
- FundsTransfer uses / calls Account B (5 Credit)
- Application service creates Account repository (6 commit)
Spot it
- Account.Transfer reaches into another account
- Business rules buried in controllers
- An entity method needs data it does not own
It costs
Too many domain services drain behaviour from entities into an anemic model.
In code
public sealed class FundsTransfer{ public void Transfer(Account from, Account to, decimal amount, decimal sentToday) { if (from.Currency != to.Currency) throw new DomainException("Accounts must share a currency."); if (sentToday + amount > 5_000m) throw new DomainException("Daily transfer limit exceeded."); from.Debit(amount); // Account guards its own overdraft rule to.Credit(amount); }}// Application service: orchestrates the use case, holds no rules.public sealed class TransferMoneyHandler( IAccountRepository accounts, IUnitOfWork unitOfWork, FundsTransfer funds){ public async Task HandleAsync(TransferMoney command, CancellationToken ct) { var from = await accounts.GetAsync(command.FromId, ct); var to = await accounts.GetAsync(command.ToId, ct); var sentToday = await accounts.SentTodayAsync(command.FromId, ct); funds.Transfer(from, to, command.Amount, sentToday); await unitOfWork.CommitAsync(ct); }}Go deeperThe full explanation and what to ask for · 1 min readExpandCollapse
Some domain operations belong to no single entity: moving money between two accounts, pricing an order with a customer's discount tier, checking that a username is unique. Forcing them onto one entity makes it reach into others. A domain service is a stateless operation, named in the ubiquitous language, that takes domain objects and applies the rule.
An application service is different: it implements a use case. It loads aggregates from repositories, calls entities and domain services, commits the transaction, publishes events and deals with authorisation and I/O. It holds no business rules; delete it and the rules still live in the domain.
Put behaviour on entities and value objects first; reach for a domain service only when no single object owns the rule. Changing two accounts in one transaction breaks the one-aggregate-per-transaction guideline. That can be a deliberate choice in a simple system; larger systems often model the transfer as its own aggregate and settle it with events.
Ask for it
- domain service
- application service
- stateless
- use case orchestration
- anemic domain model
- transaction boundary
Move the transfer rules out of TransfersController into a stateless FundsTransfer domain service (same currency, daily limit 5,000 EUR, no overdraft), and add an application service that loads both accounts, calls it, commits once and maps a rule violation to 422.