skip to content

Domain Services

Some operations belong to the domain but not to any single entity or value object, and a domain service is their home. You will learn to name them from the ubiquitous language and to keep them from becoming a dumping ground that leaves the model anemic.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

In Domain-Driven Design, what is a Domain Service, and what's a simple example of when you'd need one instead of putting a method on an entity or value object?

level: juniorimportance: must knowfreq 70%

answer

  1. stateless, verb-named
  2. escape hatch, not default
  3. cross-aggregate logic
  4. entity-first discipline
  5. still domain layer, no infra

basics

~20 s

A Domain Service holds business logic that doesn't fit on one specific object. You use one when an operation needs several objects together, or is really an action rather than a 'thing' - like transferring money between two accounts.

solid answer

~40 s

Most behavior in DDD should live on entities and value objects, keeping the model rich rather than anemic (data with logic living elsewhere). A Domain Service is the escape hatch for logic that genuinely doesn't belong to any single one of them - because it spans multiple aggregates, needs domain knowledge no single object owns (an exchange rate), or represents a process rather than a thing (transferring funds, checking a tax ID is unique). It's still part of the domain model, expressed in the ubiquitous language and free of infrastructure code - just modeled as a stateless operation instead of an object's own method. The discipline: try the entity or value object first, and reach for a domain service only when that would force one aggregate to reach into another's internals.

go deeper

for a junior

Should recognize the basic shape: an operation that doesn't fit on one object becomes a domain service, and give one plausible example.

for a middle

Should articulate the entity-first discipline and explain why defaulting to services produces an anemic model.

for a senior

Should discuss aggregate-boundary implications and give a well-reasoned example distinguishing what stays on the entity versus what moves out.

for a principal

Should connect the pattern to the risk of model erosion project-wide and describe how they'd coach a team out of a 'service-first' habit.

## The tactical building blocks Domain-Driven Design gives you a small set of tactical building blocks for expressing business behavior: - **entities** — objects with identity and a lifecycle, like an `Order` that persists and changes over time - **value objects** — immutable objects fully defined by their attributes, like a `Money` amount - **aggregates** — clusters of entities and value objects with one root and one consistency boundary - and **domain services** The default instinct should always be to put behavior on the entity or value object it most naturally belongs to, because that is what keeps a domain model **'rich'** rather than **'anemic'** - anemic meaning the objects are just data holders (getters/setters) while all the actual logic lives in separate procedural classes. A Domain Service exists for the narrower and more deliberate case where a piece of pure business logic genuinely does not belong to any single entity or value object. ## What one is, mechanically Mechanically, a Domain Service is a **stateless class or function**, named after a verb or process taken from the ubiquitous language (the shared vocabulary between developers and domain experts), that accepts domain objects as parameters and produces a domain result. - It lives in the **domain layer** - has **no framework or persistence code** inside it - and - critically - holds **no mutable instance state between calls**; every call gets everything it needs through its parameters (or through injected read-only collaborators like a repository interface) A domain service's job is narrower than orchestrating a whole use case: encapsulate one piece of business logic that would otherwise have no natural home. ## Why the pattern exists The reason this pattern exists is to prevent two bad outcomes. 1. **The first is duplicated or misplaced logic**: if a rule genuinely spans two aggregates (say, transferring funds between two `Account` aggregates), forcing it onto one of the entities means that entity now has to know about the other aggregate's internals, which breaks aggregate encapsulation and creates awkward, one-sided coupling. 2. **The second is an anemic model born from convenience**: developers under time pressure default to writing a generic '...Service' class for everything, even logic that clearly belongs on one object, because it's the path of least resistance in most application frameworks (dependency-injected, stateless, easy to test in isolation). A Domain Service is meant to be the exception, reached for deliberately, not the default dumping ground. ## The trade-off The trade-off is a **design cost** versus a **coupling cost**. Trying entity-first takes more design effort: for each new piece of logic you have to ask 'whose responsibility is this, really?' before writing the class. - Skipping that question and reaching for a domain service by default is faster in the moment but slowly hollows out the entities until none of them do anything interesting - classic anemic-domain-model rot, which then makes the code harder for domain experts to read because the business rules are scattered across generically-named service classes instead of living where the vocabulary says they should. - On the other hand, being too strict about 'everything must live on an entity' produces its own failure: entities reaching across aggregate boundaries to pull in data they don't own, or duplicating the same cross-cutting rule in three different entities because there's no shared home for it. ## Failure modes Failure modes show up in code review as recognizable smells. | Direction | The smell | |---|---| | **Overuse** | looks like a proliferation of thin, cohesion-free '...Service' or '...Manager' classes that take primitive parameters (IDs, strings) instead of domain objects, essentially reimplementing procedural transaction scripts under a DDD label | | **Underuse** | looks like an `Account` entity with a method like `transferTo(Account other, Money amount)` that has to load and mutate a second aggregate directly inside a method that's supposed to protect only its own invariants - which also tends to produce awkward transactional and concurrency problems, since aggregates are meant to be the unit of transactional consistency | ## Canonical examples - A concrete, well-known example (originating from Eric Evans' foundational description of the pattern) is exactly the **funds-transfer case**: withdrawing from one `Account` and depositing into another, with a shared rule like 'the source account must have sufficient funds,' is naturally expressed as a `FundsTransferService` that takes two `Account` aggregates and a `Money` amount, rather than as a method bolted onto `Account`. - Another canonical example is a **uniqueness check** that spans an entire collection of aggregates, such as verifying no other `Customer` already has a given email address - that rule cannot live on the `Customer` entity being created (it has nothing to compare itself against), so it becomes a domain service that collaborates with a repository to answer a purely domain question: 'is this value unique across the whole set?'

  • If you're not sure whether a piece of logic belongs on an entity or in a domain service, what question do you ask first?
    Ask whose data the logic primarily reads and changes. If it's entirely about one object's own fields, it belongs on that object. If it necessarily needs two or more aggregates, or a domain fact no single aggregate holds (like a company-wide uniqueness constraint), it's a domain-service candidate.
  • Does a Domain Service ever hold a reference to a repository?
    Yes, when the domain question genuinely requires looking beyond a single aggregate instance - for example checking uniqueness across all records. The repository is depended on through its domain-defined interface, not a concrete infrastructure class, so the domain layer stays free of persistence details.
  • How is a Domain Service different from a static utility method?
    A static utility method is usually a technical helper with no ubiquitous-language name and no domain meaning (e.g. a string formatter). A Domain Service is named after and expresses an actual business concept or process, and its inputs/outputs are domain types, not primitives.

Entities and value objects are like specialized workers who each own their own toolbox and task. A Domain Service is like a referee who steps in only for the rare play that needs to coordinate two players at once - it doesn't have its own team locker, it just enforces a rule that no single player could enforce alone.

saying these in an interview costs you the question

  • Calls every piece of logic a 'service' without asking if it belongs on an entity first
  • Can't name the business process the service represents in the ubiquitous language
  • Domain service methods take primitives (IDs, strings) instead of domain objects
  • Thinks Domain Services are just where 'shared code' goes
  • Puts persistence or HTTP calls directly inside a domain service

context

open as a page

When designing a Domain Service, how should the ubiquitous language guide its name and method signatures, and what's the tell-tale sign that you've actually built a technical helper class wearing a domain costume?

level: middleimportance: must knowfreq 65%

basics

~20 s

Name the service after the business action it performs, using words business people use - not generic tech words like Manager or Helper. If you can't explain the name in one sentence, it's not a real domain concept.

open as a page

Consider transferring money between two Account aggregates, where withdrawing from a source account and depositing into a target account must jointly enforce a rule like 'the source must have sufficient funds.' Walk through why this is best modeled as a Domain Service rather than as a method directly on the Account entity, and what trade-offs that choice introduces.

level: seniorimportance: must knowfreq 60%

basics

~20 s

Each Account should only protect its own balance rule, like not going negative. A transfer touches two accounts, so a separate FundsTransferService coordinates withdraw on one and deposit on the other, rather than one account controlling another.

open as a page

Why are Domain Services in DDD expected to be stateless, and what specifically breaks in production if a team adds mutable instance fields to one?

level: middleimportance: should knowfreq 55%

basics

~20 s

A Domain Service shouldn't remember anything between calls - each call gets everything it needs as input. If it stores data in its own fields instead, two concurrent calls can mix up or overwrite each other's data.

open as a page

A codebase has a class called OrderService with roughly 40 methods covering discount calculation, order validation, tax computation, and shipment scheduling, injected wherever an Order is touched. Is this a Domain Service in the DDD sense? What's wrong with it, and how would you fix it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

No - a real Domain Service covers one business process, not everything vaguely Order-related. This is a dumping ground that hollowed out Order. Fix it: move Order's own logic back onto Order, split the rest into small, named services.

open as a page

Can a Domain Service depend on a Repository interface, such as one used to check that no other Customer already has a given email address? Where's the line between a legitimate domain-service dependency and infrastructure concerns leaking into the domain layer?

level: principalimportance: should knowfreq 35%

basics

~20 s

Yes, when a rule needs to look across many records, like checking an email is unique. The line: depend on an interface defined by the domain, never a concrete database or HTTP class, and never let query details leak in.

open as a page