The short answer is - you can use repositories from an application service, or a domain service - but it is important to consider why, and how, you are doing so.
Purpose of a Domain Service
Domain Services should encapsulate domain concepts/logic - as such, the the domain service method:
domainService.persist(data)
does not belong on a domain service, as persist
is not a part of the ubiquitious language and the operation of persistence is not part of the domain business logic.
Generally, domain services are useful when you have business rules/logic that require coordinating or working with more than one aggregate. If the logic is only involving one aggregate, it should be in a method on that aggregate's entities.
Repositories in Application Services
So in that sense, in your example, I prefer your first option - but even there there is room for improvement, as your domain service is accepting raw data from the api - why should the domain service know about the structure of data
?. In addition, the data appears to only be related to a single aggregate, so there is limited value in using a domain service for that - generally I'd put the validation inside the entity constructor. e.g.
postAction(data){
Entity entity = new Entity(data.name, data.surname);
this.repository.persist(entity);
// ...
}
and throw an exception if it's invalid. Depending on your application framework, it may be simple to have a consistent mechanism for catching the exception and mapping it to the appropriate response for the api type - e.g. for a REST api, return a 400 status code.
Repositories in Domain Services
Notwithstanding the above, sometimes it is useful to inject and use a repository in a domain service, but only if your repositories are implemented such that they accept and return aggregate roots only, and also where you are abstracting logic that involves multiple aggregates. e.g.
postAction(data){
this.domainService.doSomeBusinessProcess(data.name, data.surname, data.otherAggregateId);
// ...
}
the implementation of the domain service would look like:
doSomeBusinessProcess(name, surname, otherAggregateId) {
OtherEntity otherEntity = this.otherEntityRepository.get(otherAggregateId);
Entity entity = this.entityFactory.create(name, surname);
int calculationResult = this.someCalculationMethod(entity, otherEntity);
entity.applyCalculationResultWithBusinessMeaningfulName(calculationResult);
this.entityRepository.add(entity);
}
Conclusion
The key here is that the domain service encapsulates a process that is part of the ubiquitous language. In order to fulfill its role, it needs to use repositories - and it's perfectly fine to do so.
But adding a domain service that wraps a repository with a method called persist
adds little value.
On that basis, if your application service is expressing a use case that calls for only working with a single aggregate, there is no problem using the repository directly from the application service.
If you need to access a database to solve your particular problem, then... Well, you need to access a database. Doesn't matter what Jimmy Bogards, Eric Evans or anyone else says. If their rules don't allow you to do that, then break their rules, follow someone else's rules, or make up your own.
When you read some rule on the internet that someone has devised, try to understand the motivations behind it first, before you apply it. That way, you have a reasonable assurance that the application of said rule aligns with your own needs and expectations.
Rules like the one you cited from Eric Evans generally have one of two motivations:
- Separation of Concerns, and
- Decoupling.
Separation of Concerns mostly means "write each class or method so that it has one area of expertise and does that well." Often, that means writing some module that specializes in database access only, allowing other modules/classes to focus on their own specific concerns without being concerned with the details of data retrieval. It also means that your classes can be "persistent-ignorant," meaning they don't have to know how to save or retrieve themselves from a database, especially some specific database technology.
Decoupling, within the context of database access, generally means one of two things:
- You want to isolate data retrieval from the rest of the system, just in case you might someday change out the database implementation for something else, or
- You want to mock the data retrieval mechanism for unit testing purposes.
There are two commonly-accepted ways to access data from a database. The first way is to use CRUD operations (create, read, update, delete). The second is to provide a Business Logic Layer. A business logic layer has methods on it like GetOffers()
which provides more intelligent (and optimized) retrieval of data than create, read, update and delete.
Now then. Jimmy Bogard's Wicked Domain Models:
What is a domain model?
An object model of the domain that incorporates both behavior and data.
Why should I care?
A lot of times – you shouldn’t.
When you should – complex domain, or a long-lived project where behavior gets added piece by piece.
So even Jimmy Bogard says "use it only if you need it."
Many software developers today suffer from "Pattern-Matching disease." They think everything in software development is a pattern, and that writing a program is an exercise in stitching patterns together. Unfortunately, that's not quite the way it works, nor is slavishly following somebody's ideas about "best practices" without understanding why.
Best Answer
Yes, that's right.
Here's what Evans wrote
Further along, he addresses your question, although somewhat obliquely
The domain service (facade) would have a dependency on the external service.
That said; looking back 10 years later, I'm not sure that he got the details of this one right. When you interact with state that is distributed somewhere else, that somewhere else may not be available. Distributed communication fails, and if that's not part of your domain, the logic for handling those failures starts to get in the way.
Gary Bernhardt and Cory Benfield both have given very interesting talks about separating side effect concerns from those of the model.
A model that transforms inputs into outputs is simple to reason about; a model that transforms inputs into outputs plus side effects, that's not simple any more.