Domain Services¶
A domain service holds domain logic that genuinely belongs to no single entity or value object. It is stateless, named in the ubiquitous language, and it lives in the domain layer.
The test for one: forcing the operation onto either participant would be a lie. A transfer between two accounts is not something the source account does to the target.
class TransferService: # domain service
def transfer(self, source: Account, target: Account, amount: Money) -> None:
if not source.can_withdraw(amount):
raise InsufficientFunds(source.id)
source.withdraw(amount)
target.deposit(amount)
Note that the rules still live on Account. The service coordinates; it does not
reimplement.
Three kinds of service¶
Confusing these is the usual source of mess.
| Layer | Knows about | Example | Contains business rules |
|---|---|---|---|
| Application service | Use cases, transactions, repositories | PlaceOrderHandler: load, call, save, commit |
No |
| Domain service | Domain concepts only | TransferService, PricingPolicy |
Yes |
| Infrastructure service | Technology | SmtpMailer, S3FileStore |
No |
graph TD
API[Controller] --> APP[Application service<br/>orchestrates one use case]
APP --> REPO[Repository]
APP --> DS[Domain service<br/>business rule spanning objects]
DS --> E[Entities and value objects]
APP --> INF[Infrastructure service<br/>email, storage]
An application service that contains an if about business rules has stolen work from
the domain. A domain service that opens a transaction or sends an email has stolen work
from the other two layers.
When a domain service is right¶
- The operation spans several aggregates, such as transferring money or checking a booking against every other booking for a room.
- The rule is a policy that varies, such as pricing, discounting or risk scoring,
where a
PricingPolicyinterface with several implementations is honest. - It needs external domain knowledge, such as an exchange rate. The service takes an interface declared in the domain, and infrastructure implements it.
class BookingService:
def book(self, room: Room, period: DateRange, existing: list[Booking]) -> Booking:
if any(b.period.overlaps(period) for b in existing):
raise RoomUnavailable(room.id)
return Booking.create(room.id, period)
When it is wrong¶
The dominant failure is using services as the default home for all behaviour, which produces an anemic domain model: entities with getters and setters, and every rule scattered across service classes.
Before writing a service, check in order:
- Does this rule concern one object's own data? Put it on that entity or value object.
- Does it concern an aggregate's members? Put it on the aggregate root.
- Is it only orchestration, loading and saving? That is an application service.
- Only what survives all three is a domain service.
Other signs it has gone wrong:
*Manager,*Helper,*Util. Names outside the ubiquitous language, and usually a bag of unrelated functions.- Stateful services. State belongs in the model. A service holding fields between calls is an entity in disguise.
- A service per entity.
OrderService,CustomerService,InvoiceServicematching the entities one for one is the anemic pattern with extra steps.
Keep them small and named honestly¶
A good domain service is one operation with a name a domain expert would recognise:
TransferService, OverbookingPolicy, CreditRiskAssessment. If the name needs "and"
to describe it, split it.
Check Your Understanding¶
Transferring money between two accounts is implemented as a domain service. Why not put it on Account?
What separates an application service from a domain service?
A codebase has OrderService, CustomerService and InvoiceService, one per entity, holding all the rules. What is this?