Appearance
Domain Service
Encapsulating domain operations that don't naturally belong to entities or value objects
Related Concepts: Repository | Use Cases | Clean Architecture
What is a Domain Service?
"Sometimes, it just isn't a thing." - Eric Evans, Domain-Driven Design
A Domain Service is an operation offered as an interface that stands alone in the model, without encapsulating state. When important domain operations don't naturally fit within an Entity or Value Object, a Domain Service provides a home for that behavior without compromising the clarity of your domain model.
As Eric Evans explains:
"Some concepts from the domain aren't natural to model as objects. Forcing the required domain functionality to be the responsibility of an ENTITY or VALUE either distorts the definition of a model-based object or adds meaningless artificial objects."
Domain Services exist for operations that are fundamentally activities or actionsverbs rather than nounsthat have significant meaning within your domain's Ubiquitous Language.
The Three Characteristics
According to Eric Evans, a good Domain Service has three essential characteristics:
1. The operation relates to a domain concept that is not a natural part of an Entity or Value Object
The behavior doesn't have a natural home in any single domain object. Forcing it into an entity would distort that entity's purpose and create awkward dependencies.
2. The interface is defined in terms of other elements of the domain model
Domain Services work exclusively with domain typesentities, value objects, and other domain services. They speak the language of the domain, not the language of infrastructure.
3. The operation is stateless
Any client can use any instance of a Domain Service without regard to that instance's history. As Evans clarifies: "The execution of a SERVICE will use information that is accessible globally, and may even change that global information (that is, it may have side effects). But the SERVICE does not hold state of its own that affects its own behavior."
When to Use Domain Services
Create a Domain Service When:
- Logic spans multiple aggregates or entities - The operation naturally coordinates several domain objects
- Complex business operations don't belong to a single entity - The behavior would create awkward responsibilities if forced into an entity
- Preventing domain logic duplication - The operation would otherwise scatter throughout your codebase
- Business rules require coordination - Multiple domain objects must collaborate to enforce a rule
- The operation is intrinsically an activity - It's a verb (transfer, calculate, analyze) rather than a noun
Don't Create a Domain Service When:
- The behavior naturally belongs to an entity - As Evans warns, "the more common mistake is to give up too easily on fitting the behavior into an appropriate object"
- It's just infrastructure - Technical operations like sending emails belong in infrastructure services
- It's just orchestration - Workflow coordination without business logic belongs in application services
- You're creating "Manager" objects - Services that masquerade as meaningless "doers" often indicate poor domain modeling
Domain vs Application vs Infrastructure Services
One of the most important distinctions in service-oriented architectures is understanding which layer a service belongs to. Evans provides clear guidance:
Domain Services
- Have business meaning in the domain language
- Embed significant business rules and domain logic
- Make business decisions about domain state
- Work with domain types (entities, value objects)
- Example:
FundsTransferService- encapsulates business rules for debiting/crediting accounts, determines if transfer is allowed
Application Services
- Lack business meaning - they don't contain domain knowledge
- Orchestrate workflows - coordinate domain operations without making business decisions
- Handle technical concerns - message formats, session management, transaction boundaries
- Work with DTOs and domain types - translate between external representations and domain model
- Example:
FundsTransferApplicationService- parses XML requests, calls domain service, sends notifications
Infrastructure Services
- Purely technical with no business meaning whatsoever
- Provide technical capabilities - email, file storage, message queues
- Example:
NotificationService- sends emails and SMS messages
Learn more about the distinction from Enterprise Craftsmanship
Examples
Example 1: Funds Transfer (Banking Domain)
From Eric Evans' original example:
typescript
// Domain Service - contains business logic
class FundsTransferDomainService {
constructor(
private accountRepository: AccountRepository,
private ledgerService: LedgerService
) {}
async transfer(
fromAccountId: AccountId,
toAccountId: AccountId,
amount: Money
): Promise<TransferResult> {
const [fromAccount, toAccount] = await Promise.all([
this.accountRepository.findById(fromAccountId),
this.accountRepository.findById(toAccountId)
]);
if (!fromAccount || !toAccount) {
return TransferResult.failure('Account not found');
}
// Business rule: verify sufficient funds
if (!fromAccount.hasSufficientFunds(amount)) {
return TransferResult.failure('Insufficient funds');
}
// Business rule: verify accounts are active
if (!fromAccount.isActive() || !toAccount.isActive()) {
return TransferResult.failure('Inactive account');
}
// Execute the transfer using domain logic
fromAccount.debit(amount);
toAccount.credit(amount);
await Promise.all([
this.accountRepository.save(fromAccount),
this.accountRepository.save(toAccount),
this.ledgerService.recordTransfer(fromAccount, toAccount, amount)
]);
return TransferResult.success();
}
}Example 2: Shipping Cost Calculator (E-commerce)
A classic example where the calculation logic doesn't naturally belong to Order or Product:
typescript
// Domain Service - encapsulates complex shipping business rules
class ShippingCostCalculator {
calculateShippingCost(
order: Order,
destination: Address,
shippingMethod: ShippingMethod
): Money {
// Business rules for shipping calculation
const baseRate = shippingMethod.getBaseRate();
const weight = order.calculateTotalWeight();
const dimensions = order.calculatePackageDimensions();
// Apply business rules
let cost = baseRate.multiply(weight);
// Oversized package surcharge
if (dimensions.isOversized()) {
cost = cost.add(Money.dollars(25));
}
// Free shipping promotion
if (order.qualifiesForFreeShipping()) {
return Money.zero();
}
// Destination-based adjustments
if (destination.isRemote()) {
cost = cost.multiply(1.5);
}
return cost;
}
}More examples at DEV Community
Example 3: Library Book Lending (Library Domain)
When business logic requires coordinating multiple entities:
typescript
// Domain Service - handles complex borrowing business rules
class BookLendingService {
constructor(
private bookRepository: BookRepository,
private readerRepository: ReaderRepository
) {}
async canBorrowBook(
readerId: ReaderId,
bookId: BookId
): Promise<BorrowingEligibility> {
const [reader, book] = await Promise.all([
this.readerRepository.findById(readerId),
this.bookRepository.findById(bookId)
]);
if (!reader || !book) {
return BorrowingEligibility.notFound();
}
// Business rule: check reader account status
if (!reader.isActive()) {
return BorrowingEligibility.ineligible('Account is not active');
}
// Business rule: check borrowing limit
if (reader.hasReachedBorrowingLimit()) {
return BorrowingEligibility.ineligible('Borrowing limit reached');
}
// Business rule: check book availability
if (!book.hasAvailableCopies()) {
return BorrowingEligibility.ineligible('No copies available');
}
// Business rule: reading room only restriction
if (book.isReadingRoomOnly()) {
return BorrowingEligibility.readingRoomOnly();
}
return BorrowingEligibility.eligible();
}
async borrowBook(readerId: ReaderId, bookId: BookId): Promise<Loan> {
const eligibility = await this.canBorrowBook(readerId, bookId);
if (!eligibility.isEligible()) {
throw new BorrowingNotAllowedError(eligibility.reason);
}
// Execute the domain operation
const reader = await this.readerRepository.findById(readerId);
const book = await this.bookRepository.findById(bookId);
const loan = book.lendTo(reader);
reader.addLoan(loan);
await Promise.all([
this.bookRepository.save(book),
this.readerRepository.save(reader)
]);
return loan;
}
}Read more about this pattern at Developer20
Naming Domain Services
Domain Services should be named after activities in your Ubiquitous Language:
DO
FundsTransferService- describes the domain activityShippingCostCalculator- names the domain calculationBookLendingService- captures the domain operationInventoryAllocationService- describes the business action
DON'T L
AccountManager- meaningless "manager" suffixBookHelper- vague "helper" indicates unclear responsibilityDataProcessor- technical term, not domain languageOrderHandler- generic "handler" lacks domain meaning
As Evans warns: "Services masquerade as model objects, appearing as objects with no meaning beyond doing some operation. These 'doers' end up with names ending in 'Manager' and the like."
Granularity and Reusability
Evans emphasizes that Domain Services also serve to control granularity:
"Medium-grained, stateless SERVICES can be easier to reuse in large systems because they encapsulate significant functionality behind a simple interface."
Domain Services prevent fine-grained domain objects from leading to:
- Knowledge leaks - domain behavior coordinated in the application layer
- Inefficient messaging - chatty interfaces in distributed systems
- Scattered domain logic - business rules spread across multiple layers
By consolidating related domain operations, Domain Services keep domain knowledge concentrated where it belongs.
Best Practices
DO
- Define services in the domain layer - they are part of your domain model
- Name operations from Ubiquitous Language - use terms meaningful to domain experts
- Keep services stateless - any instance should be usable by any client
- Accept and return domain types - entities, value objects, domain events
- Use judiciously - don't strip all behavior from entities
- Test as pure domain logic - minimal mocking required
DON'T L
- Create services reflexively - try to fit behavior into entities first
- Mix domain and application concerns - keep orchestration out of domain services
- Depend on infrastructure directly - use domain-defined repository interfaces
- Create "Manager" or "Helper" classes - these often lack true domain meaning
- Make services stateful - state belongs in entities and value objects
- Let services grow too large - break up complex services by subdomain
Testing Domain Services
Domain Services are highly testable because they contain pure domain logic:
typescript
describe('FundsTransferDomainService', () => {
let service: FundsTransferDomainService;
let accountRepository: InMemoryAccountRepository;
beforeEach(() => {
accountRepository = new InMemoryAccountRepository();
service = new FundsTransferDomainService(
accountRepository,
new InMemoryLedgerService()
);
});
it('should reject transfer when insufficient funds', async () => {
const fromAccount = new Account('acc-1', Money.dollars(50));
const toAccount = new Account('acc-2', Money.dollars(100));
await accountRepository.save(fromAccount);
await accountRepository.save(toAccount);
const result = await service.transfer(
fromAccount.id,
toAccount.id,
Money.dollars(100) // More than available
);
expect(result.isFailure()).toBe(true);
expect(result.reason).toBe('Insufficient funds');
});
it('should successfully transfer funds between accounts', async () => {
const fromAccount = new Account('acc-1', Money.dollars(200));
const toAccount = new Account('acc-2', Money.dollars(100));
await accountRepository.save(fromAccount);
await accountRepository.save(toAccount);
const result = await service.transfer(
fromAccount.id,
toAccount.id,
Money.dollars(50)
);
expect(result.isSuccess()).toBe(true);
const updatedFrom = await accountRepository.findById('acc-1');
const updatedTo = await accountRepository.findById('acc-2');
expect(updatedFrom.balance).toEqual(Money.dollars(150));
expect(updatedTo.balance).toEqual(Money.dollars(150));
});
});Key Takeaways
- Domain Services exist for operations that are fundamentally activities, not things
- They must have clear business meaning - not just technical helpers
- Statelessness is essential - any instance can serve any client
- They work exclusively with domain types - entities, value objects, other domain services
- Use them judiciously - don't give up on entity behavior too easily
- Avoid meaningless names - "Manager" and "Helper" often indicate poor modeling
- They're distinct from application and infrastructure services - keep the layers clear
Further Reading
Primary Sources
- Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans (2003) - The definitive source on Domain Services
- Domain-Driven Design - Martin Fowler's overview of DDD concepts
Supporting Articles
- Domain services vs Application services - Vladimir Khorikov's clear distinction between service types
- Services in Domain-Driven Design - Lev Gorodinski's detailed explanation
- Domain Services and Factories in Domain-Driven Design - Practical examples and guidance
- Services in DDD finally explained - Clear examples of service coordination
- Domain Driven Design: Domain Service, Application Service - Community discussion on Stack Overflow
For implementation examples, see Application Service for the complementary orchestration pattern.