Skip to main content

Patternintermediate

Domain Service

Overview

A domain service holds business rules that do not naturally belong to any entity or value object — typically because they involve several of them.

It is still domain: it knows no infrastructure, orchestrates no transaction, knows nothing of HTTP or of the database.

Problem

Not every rule fits in an entity.

"Transfer an amount between two accounts" involves two accounts and belongs to neither — putting it in Account would make one account know and modify another, which violates the aggregate boundary.

"Assess eligibility" may depend on three different aggregates.

With no place for those rules, they migrate into the application service — where they get mixed with orchestration and transaction control — or into an entity that comes to know too much.

The domain service is the correct place for them.

Core Concepts

The criterion

A rule belongs in a domain service when:

It is of the domain — it expresses a business decision, not technical coordination.

It belongs to no entity — forcing it into one produces unnatural coupling.

It is stateless — the service keeps nothing between calls.

Missing any of the three, it is something else.

Domain service versus application service

The distinction that causes the most confusion, and the reason Onion names both rings.

Domain serviceApplication service
ContainsBusiness rulesOrchestration
Knows infrastructureNoYes
Controls the transactionNoYes
Decides something about the businessYesNo
Testable without infrastructureYesNeeds substitutes

The practical test: if you removed all the technology, would this rule still exist? If so, it is domain.

It speaks the ubiquitous language

A domain service is named after a business operation: EligibilityAssessor, ShippingCalculator, TransferBetweenAccounts.

Names like OrderManager, CustomerHelper or GenericProcessor are a sign the rule was not understood — and frequently that the service became a dumping ground.

The risk: escaping responsibility

The mode of degeneration is well known: it is easier to write the rule in a service than to find where it belongs in the entity.

The result is the anemic model — entities with no behaviour and services with all the logic. See encapsulation.

The check: before creating a domain service, apply the three conditions from the definition — it is domain rule, it belongs to no entity, it is stateless. "Involves more than one aggregate" is a heuristic, not a test: it is right most of the time, and it fails in cases like the ShippingCalculator above, which meets the three conditions over a single aggregate. When the rule involves one aggregate and fits in it, it is that aggregate's.

When to Use

  • The rule involves more than one aggregate.
  • The rule is domain, not coordination.
  • There is no entity where it fits without producing unnatural coupling.
  • The operation has a name in the business's vocabulary.

When Not to Use

When the rule involves a single aggregate. It belongs to that aggregate.

When it is orchestration. That is an application service.

When the service would hold state. Domain services are stateless; state belongs to entities.

As a dumping ground for logic with no obvious home. It is the most common degeneration, and the symptom is a generic name.

In subdomains outside the core. The distinction between the rings rarely pays off there.

Alternatives

  • A method on the entity — when it involves a single aggregate.
  • A value object with behaviour — when the rule is about a concept, not about entities.
  • A factory method — when the rule is about creation. See factory.
  • A policy as an object — a rule encapsulated as a Strategy, when there are variants.

Trade-offs

Domain serviceRule on the entity
A rule across aggregates has a homeOne entity would know another
Testable in isolationTestable along with the entity
Risk of hollowing out the entitiesA rich model
One more typeNone
The domain name made explicitThe rule inside a method

Failure Modes

A service that becomes a dumping ground. A generic name, many unrelated operations.

Anemic model. Rules that belonged to the entities migrated into services.

Stateful service. Shared between requests, it produces concurrency defects.

A service that knows infrastructure. It stopped being domain.

A domain service that orchestrates. Confusion with the application layer.

Common Mistakes

Creating a service for a rule involving one aggregate. The dominant mistake.

A generic name. Manager, Helper, Processor, Handler — none is domain vocabulary.

Injecting a repository into the domain service. It is debated; this material's position is that the domain service should receive the aggregates already loaded, keeping data access in the application layer. That preserves testability without substitutes.

Confusing it with an application service.

Real-World Example

A banking system had a TransferService with 400 lines, containing: balance validation, fee calculation, daily limit checking, audit logging, email notification and transaction control.

Six responsibilities: three of domain and three not. Two belonged to Account, one to a domain service, and the other three — auditing, notification and transaction control — to the application layer and to effects.

The separation:

The rule that belonged to the entity. Balance validation and the daily limit are invariants of Account. They moved there — account.debit(amount) throws if the balance or the limit does not allow it.

The rule that belonged to a domain service. The fee calculation depends on the type of both accounts, the amount and the time of day. It belongs to neither account. It became FeeCalculator, stateless, testable with two in-memory objects.

Coordination. Loading the accounts, calling the debit and the credit, computing the fee, persisting, controlling the transaction, publishing the event — all of that went to the application service.

Effects. Auditing and notification became consumers of a domain event, outside the transactional flow.

The most relevant result was not the organization. It was that FeeCalculator came to have 40 unit tests running in milliseconds, covering combinations of account type, amount and time of day that previously required setting up two accounts in the database.

The fee rule changed three or four times a year by commercial decision. The change cycle dropped from days to hours.

Practical Exercise

List the classes in your system whose names end in Service, Manager or Handler.

For each of their operations, answer: is it a business rule or coordination? Does it involve one aggregate or several?

The rules involving a single aggregate are in the wrong place.

Interview Questions

  • What is the criterion for a rule to belong to a domain service?
  • How do you distinguish a domain service from an application service?
  • Why should domain services be stateless?

Further Exploration

  • Evans, Eric. Domain-Driven Design. Addison-Wesley, 2003.
  • Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013.
  • Fowler, Martin. AnemicDomainModel, 2003.
Finished reading this document?Your progress is saved in this browser only.