I've been learning and applying Domain-Driven Design (DDD) principles for a few years, and I often struggle with distinguishing between domain and application services. In this article, I’d like to share some observations and thoughts based on my experience. Hopefully, you’ll find something useful here!
Approach
I’ll first explore services in a complex domain and then in a simple one. After that, I’ll summarize my thoughts.
A Complex Domain
It seems easier to separate domain and application services in a complex domain. Let’s illustrate this with an example.
Imagine a banking application where we need logic to transfer money from one account to another. Below is a simplified snippet demonstrating possible application and domain services for that purpose:
// src/Domain/Entity/Account.ts
class Account {
balance: number;
}
// src/Domain/Service/MoneyTransferDomainService.ts
class MoneyTransferDomainService {
async transferMoney(fromAccount: Account, toAccount: Account, amount: number): Promise<void> {
if (fromAccount.balance < amount) {
throw new Error('Insufficient funds.');
}
fromAccount.balance -= amount;
toAccount.balance += amount;
}
}
// src/Application/MoneyTransferApplicationService.ts
class MoneyTransferApplicationService {
async transferMoney(fromAccountId: number, toAccountId: number, amount: number) {
const fromAccount = await this.accountRepository.findOneById(fromAccountId);
const toAccount = await this.accountRepository.findOneById(toAccountId);
if (!fromAccount) {
throw new Error(`Account with id "${fromAccountId}" not found`);
}
if (!toAccount) {
throw new Error(`Account with id "${toAccountId}" not found`);
}
await this.moneyTransferDomainService.transferMoney(fromAccount, toAccount, amount);
}
}
In this example, the MoneyTransferDomainService
encapsulates a crucial business rule: moving money between accounts. It belongs in the domain layer because it represents core business logic. Meanwhile, the MoneyTransferApplicationService
acts as a coordinator, retrieving the necessary entities and delegating the core transfer logic to the domain service.
This separation makes sense. We have a main part that belongs to domain layer and a secondary that belongs to application layer. Let's explore at a few alternatives applicable to this example.
Possible Alternatives
1. Placing the logic inside the Account
entity
- This could lead to bloated entities (a “god class”).
- It sometimes unclear which entity should contain the logic when multiple entities are involved. In a more complex example with
PersonalAccount
andBusinessAccount
, it would be harder to decide where to place this logic.
2. Moving the logic to MoneyTransferApplicationService
- This would cause domain logic to leak into the application layer.
- The application layer would gain more responsibilities, some of which may seem redundant.
Hints for Identifying a Domain Service
- The logic is crucial for the business.
- The logic involves interactions between multiple entities.
A Simple Domain
Now, let’s consider an example from a simpler domain. Suppose we have a user management component where we need to update a User
entity. Here’s a relevant snippet:
// src/Domain/Entity/User.ts
class User {
name: string;
email: string;
}
// src/Application/UpdateUserApplicationService.ts
class UpdateUserApplicationService {
async update(id: number, name: string, email: string) {
const user = await this.userRepository.findOneById(id);
if (!user) {
throw new Error(`User with id "${id}" not found`);
}
user.name = name;
user.email = email;
}
}
Here, we only have an application service, which looks okay since updating a user’s details does’t look like a complex business rules that should be encapsulated in the heart of the application.
If there is no significant business rules it's hard to find reasons moving such logic into domain layer. In such cases having only application services look enough.
A Few Final Thoughts
So, what’s the difference then? It's still a tough question for me. But one guideline seems clear: domain services should encapsulate significant business rules. The more essential a piece of logic is to the business, the more it navigates toward the domain layer. Conversely, if the logic is less central to the business or mainly coordinates processes, it tends to the application layer. However, this distinction isn’t always straightforward.
At times, I question whether creating domain services is even necessary. In such cases, Clean Architecture principles can be appealing. Instead of debating which layer a piece of logic belongs to, Clean Architecture offers a clear answer: it should be placed within a use case.
Additional Resources
If you’d like to dive deeper into these topics, here are some useful references:
- Domain-Driven Design by Eric Evans (Chapter Five: A Model Expressed in Software – Services)
- Clean Architecture by Robert C. Martin (Part V: Architecture, Chapter 22: The Clean Architecture – Use Cases)
- Domain-Driven Design vs. Clean Architecture – Khalil Stemmler
This is my first post—hope you found it helpful! :)
Top comments (0)