I'm diving into the concepts to Domain-Driven Design (DDD) and found some principles strange, especially regarding the isolation of domain and persistence model. Here is my basic understanding:
- A service on the application layer (providing a feature set) requests domain objects from a repository it needs to carry out its function.
- The concrete implementation of this repository fetches data from the storage it was implemented for
- The service tells the domain object, which encapsulates business logic, to perform certain tasks which modifies its state.
- The service tells the repository to persist the modified domain object.
- The repository needs to map the domain object back to the corresponding representation in storage.
Now, given the above assumptions, the following seems awkward:
The domain model seems to load the entire domain object (including all fields and references), even if they are not needed for the function that requested it. Loading entirely might not even be possible if other domain objects are referenced, unless you load those domain objects as well and all the objects they reference in turn, and so on and so forth. Lazy loading comes to mind, which however means that you start querying your domain objects which should be the responsibility of the repository in the first place.
Given this problem, the "correct" way of loading domain objects seems to be having a dedicated loading function for each use case. These dedicated functions would then only load the data required by the use case they were designed for. Here's where the awkwardness comes into play: First, I would have to maintain a considerable amount of loading functions for each implementation of the repository, and domain objects would end up in incomplete states carrying
null in their fields. The latter should technically not be a problem because if a value was not loaded, it should not be required by the functionality that requested it anyway. Still it's awkward and a potential hazard.
How would a domain object verify uniqueness constrains upon construction if it does not have any notion of the repository? For instance, if I wanted to create a new
User with a unique social security number (which is given), the earliest conflict would occur upon asking the repository to save the object, only if there is a uniqueness constraint defined on the database. Otherwise, I could look for a
User with the given social security and report an error in case it exists, before creating a new one.
But then the constraint checks would live in the service and not in the domain object where they belong. I just realised that the domain objects are very well allowed to use (injected) repositories for validation.
I perceive the mapping of domain objects to a storage backend as a work-intensive process in comparison to having the domain objects modify the underlaying data directly. It is, of course, an essential prerequisite to decouple the concrete storage implementation from the domain code. However, does it indeed come at such a high cost?
You apparently have the option to use ORM tools to do the mapping for you. These would often require you to design the domain model according to the ORM's restrictions, however, or even introduce a dependency from the domain to infrastructure layer (by using ORM annotations in the domain objects, for instance). Also I've read that ORMs introduce a considerable computational overhead.
In the case of NoSQL databases, for which hardly any ORM-like concepts exist, how do you keep track of which properties changed in the domain models upon
Edit: Also, in order for a repository to access the domain object's state (i.e. the value of each field), the domain object needs to reveal its internal state which breaks encapsulation.
- Where would transactional logic go? This is certainly persistence specific. Some storage infrastructure might not even support transactions at all (like in-memory mock repositories).
- For bulk operations that modify multiple objects, would I have to load, modify and store each object individually in order to go through the object's encapsulated validation logic? That is opposed to executing a single query directly onto the database.
I would appreciate some clarification on this topic. Are my assumptions correct? If not, what is the correct way of tackling these problems?