https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/ As I mentioned above, the role of an Application Service is to:
- use a repository to find one or several entities;
- tell those entities to do some domain logic;
- and use the repository to persist the entities again, effectively saving the data changes.
However, sometimes we encounter some domain logic that involves different entities, of the same type or not, and we feel that that domain logic does not belong in the entities themselves, we feel that that logic is not their direct responsibility.
So our first reaction might be to place that logic outside the entities, in an Application Service. However, this means that that domain logic will not be reusable in other use cases: domain logic should stay out of the application layer!
The solution is to create a Domain Service, which has the role of receiving a set of entities and performing some business logic on them. A Domain Service belongs to the Domain Layer, and therefore it knows nothing about the classes in the Application Layer, like the Application Services or the Repositories. In the other hand, it can use other Domain Services and, of course, the Domain Model objects.
Exemple sans domain service
public class TransferMoneyApplicationService {
public void transfer(AccountId fromId, AccountId toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
// ❌ BUSINESS RULE IN APPLICATION LAYER
if (amount.compareTo(new BigDecimal("10000")) > 0) {
throw new IllegalArgumentException("Daily transfer limit exceeded");
}
from.withdraw(amount);
to.deposit(amount);
accountRepository.save(from);
accountRepository.save(to);
}
}- Not reusable in another use case (e.g. scheduled transfers, batch payments)
- Hard to test independently from infrastructure
Avec domain service
public class MoneyTransferService {
private static final BigDecimal DAILY_LIMIT = new BigDecimal("10000");
public void transfer(Account from, Account to, BigDecimal amount) {
if (amount.compareTo(DAILY_LIMIT) > 0) {
throw new IllegalArgumentException("Daily transfer limit exceeded");
}
from.withdraw(amount);
to.deposit(amount);
}
}- Pure domain logic ⇒ easy to test
⇒ Application Service devient plus simple
// Rename in UseCase
public class TransferMoneyUseCase {
public void execute(AccountId fromId, AccountId toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
moneyTransferService.transfer(from, to, amount);
accountRepository.save(from);
accountRepository.save(to);
}
}Domain Service optionnel
Un Domain Service est nécessaire lorsque une règle métier implique plusieurs agrégats et qu’elle ne peut pas être naturellement rattachée à l’un d’eux sans violer le principe de responsabilité unique. En revanche, si une seule entité (ou agrégat) est concernée et que la règle protège ses invariants, alors le comportement doit être implémenté directement dans le modèle de domaine. L’Application Service se limite alors à orchestrer : charger l’agrégat, invoquer son comportement métier, puis le persister.
public class DepositMoneyUseCase {
public void execute(AccountId accountId, BigDecimal amount) {
Account account = accountRepository.findById(accountId);
account.deposit(amount); // pas de Domain Service
accountRepository.save(account);
}
}Est-ce domain logic ou application service
A challenging activity for many DDD practitioners is drawing the line between application logic and domain logic. - Patterns, Principles, and Practices of Domain-Driven Design p700
Exemple : Lorsqu’un client parraine un ami, les deux reçoivent un bon d’achat de 10€ et le parrain gagne des points de fidélité.
Ce service ne contient pas la logique métier, il coordonne les objets du domaine.
public class ReferralApplicationService {
private final CustomerRepository customerRepository;
private final ReferralPolicy referralPolicy;
private final Logger logger;
public void referFriend(CustomerId referrerId, CustomerRegistration friendDetails) {
try {
// récupérer le parrain
Customer referrer = customerRepository.findById(referrerId);
// créer le nouveau client
Customer friend = customerRepository.add(friendDetails);
// appliquer la règle métier
referralPolicy.apply(referrer, friend);
logger.info("Parrainage effectué avec succès");
} catch (Exception e) {
logger.error("Erreur lors du parrainage", e);
}
}
}La vraie règle métier se trouve ici, dans le Domain Service.
public class ReferralPolicy {
public void apply(Customer referrer, Customer friend) {
// règle métier
referrer.addLoyaltyPoints(100);
referrer.addVoucher(new Money(10));
friend.addVoucher(new Money(10));
referrer.promoteToGoldIfEligible();
}
}On peut reconnaître une fuite de logique métier, si par exemple l’application service ferait ceci
referrer.addVoucher(10);
friend.addVoucher(10);
referrer.addLoyaltyPoints(100);
referrer.promoteToGoldIfEligible();Egalement on peut se poser les questions suivantes
- “Est-ce que ces étapes doivent toujours se produire ensemble ?”
- “Ces étapes sont-elles inséparables ?”
⇒ Si oui, alors c’est une règle métier. Donc cela doit vivre dans le domaine (souvent un Domain Service ou une Policy)
Quand un parrainage est validé :
- le parrain reçoit des points
- les deux reçoivent un bon
- le statut peut évoluer
⇒ Ces actions sont indissociables.