I'm trying to wrap my head around the best possible solution in the following situation:
When updating part of an aggregate, could be any part of the aggregate so either the root or any other entity, how could these changes be persisted back to the db layer.
There have been a lot of solutions on StackExchange advising something like using your ORM models as Domain objects so you could change the attribute on the Aggregate and let the ORM layer diff and flush the changes to the db, most examples contain references to the Enity Framework if I'm not mistaken. Like the solution here, Article which is a Domain object contains ORM persistence logic
My partial understanding of DDD was that you shouldn't define any persistence layer logic in your domain models. The persistence logic should be defined in your repository, possibility of having a variety of persistence mechanisms (Postgres, MongoDB, S3 etc). Also 'littering' Domain models with persistance logic and/or 'original' SQL objects makes it a lot harder to test my Domain objects / Aggregates.
I'm having trouble understanding an easy and simple solution, maybe there isn't any, on how to map these changes back to my ORM layer, in my case I'm using Postgres. Unless the answer is a strong mapping between your ORM model and Domain model in your repository it looks really hard and verbose to do so.
I figured out, by reading other solutions, that there are a couple of other possibilities (they all have their own drawbacks):
Generate a
ModelAttributeChanged
event whenever you change something in your Domain model. You could either return this event, or store it somewhere on the Domain model. When persisting this model in the repository, query the ORM model first and map these changes back on the ORM model before committing it.changed_name_event = person_aggregate.set_name('Henk') Repository.save(person_aggregate, changed_name_event)
After you change something on the Aggregate, explicitly call the update method on the Repository aswell to update the attribute. You'd need to update everything on both the Aggregate and on the repository, and you need to know in advance which attributes of your aggregate are going to change before calling the right method on your repository.
person_aggregate.change_name('Henk') repository.change_person_name(person_aggregate, 'Henk')
Ideally I just want to be able to update my aggregate and save it through my repository. However because I map my ORM model to an AR, Aggregate Root, I 'loose' the mapping to the ORM model. I could ofcourse keep track of all the changes I make to the Aggregate and whenever I call the repository apply these changes to the ORM model and commit it to the database. My 'problem' with this solution is that I need a strong mapping and tracking changes for nested entities is a hard, complex and error prone process.
If this is the necessary evil in order completely isolate your domain logic I'm okay with it but it just feels like a lot of logic has to be defined in order to get this abstraction working.
4 Answers 4
Ideally I just want to be able to update my aggregate and save it through my repository.
- Save the entire aggregate every time it changes regardless of which (possibly nested) fields change:
Repository.save(person_aggregate)
In all seriousness, I'd actually like to address a different question above - one that was not specifically denoted (Robert Harvey's answer touches on this as well).
Phrases like "best possible solution" are, at best, misleading, and at worst, obfuscating. Every design decision you make is a trade off. Understanding these trade offs and making the "best possible" choice between them is precisely the job of an architect. Unfortunately for us, we cannot know for what you would like to optimize or what levels of compromise your system can tolerate.
Me? I think in terms of complexity. I would not go down the road of manually implementing change-tracking (worst option). Nor would I introduce some variant of event sourcing (second worst option). I also wouldn't duplicate/propagate changes in every use-case. Where does that leave you?
You seem to have a reasonable grasp of the trade offs between the different options. Now be an architect and pick one.
-
1I'd just like to make a note here that the spirit of this answer is meant to be interpreted as a "kick in the butt" or a "you can do it". I apologize if it reads as condescension. That is not my intent.user3347715– user334771505/05/2020 16:48:23Commented May 5, 2020 at 16:48
-
I agree with "Save the entire aggregate every time it changes regardless of which (possibly nested) fields change". The optimization problem has nothing to do with DDD, and which can be solved separately.neuro_sys– neuro_sys01/15/2021 09:10:47Commented Jan 15, 2021 at 9:10
The purpose of aggregates is to model complex business relationships and to take business actions. The purpose of your repository is to perform ordinary CRUD operations on a data store.
Let's walk through a simple example: an Invoice. Invoices are not simple CRUD entities; they are aggregates of several entities: Customer, Products, Addresses, Payments. You operate on these entities individually using CRUD, but you operate on an invoice using methods that apply to an invoice.
Some potential methods for an invoice:
- Get Balance
- Add Products
- Change a Quantity
So your Invoice object becomes a mapping between methods that apply to an Invoice, and CRUD operations that apply to the respective repositories. It adds additional value beyond merely separating your persistence mechanism from the entities it persists.
I think when people start working through architectures, they forget to think about the reasons they're creating an architecture in the first place. You don't create an architecture to conform to a set of architectural rules; you create an architecture to make your software maintainable. The architecture is there to serve you, not the other way around.
Further Reading
Clean Architecture - Too many Use Case Classes
-
Okay so correct me if I'm wrong, you are basically advocate for modelling Domain models in a more 'pragmatic' way? Allowing Aggregates to be fitted as ORM objects gives us the flexibility of saving them in the repository while also centralizing the business logic for clustered behavior?ExcellentAverage– ExcellentAverage04/04/2020 18:31:09Commented Apr 4, 2020 at 18:31
-
Well, the Customer, Product, Address and Payment entities are fitted as ORM objects. The Invoice object just aggregates those entities and performs business operations on them from an Invoice perspective.Robert Harvey– Robert Harvey04/04/2020 18:35:45Commented Apr 4, 2020 at 18:35
-
1But the invoice object would have to be an entity as well right? since it is the Aggregate Root (AR) of the Invoice?ExcellentAverage– ExcellentAverage04/04/2020 18:39:32Commented Apr 4, 2020 at 18:39
-
True, but saving an invoice header record is a simple CRUD operation, just like it is with the other entities.Robert Harvey– Robert Harvey04/04/2020 18:52:02Commented Apr 4, 2020 at 18:52
My partial understanding of DDD was that you shouldn't define any persistence layer logic in your domain models.
The basis for that advice (at least the versions of that advice which I would call good advice) is not that you should design your domain models with persistence in mind, it's about having your ORM make use of your domain models as they are.
Generally speaking in DDD you're working with some kind of oniony/domain-centric approach where the persistence layer has access to your domain, which is the basis for being able to assert that the persistence layer can handle your domain model.
Based on the direction of that relationship, it's not okay for the domain to build itself based on what the persistence layer needs, but it's perfectly okay for the persistence layer to orient itself around handling the domain models - which including any persistence logic centered around the specific structure of the domain model.
However, that advice only applies in as much is possible. Sometimes your domain models simply are not compatible with your chosen persistence method. At that stage, you will have to create separate persistence models that are compatible with the storage device, and you will have to map from your domain models to your persistence models (and vice versa when retrieving).
But that mapping logic is owned by the persistence layer (since the persistence models are private to that layer), which doesn't really change what I said before: the persistence layer is still the layer that handles the incoming domain models and figures out how to store them.
Whether it's possible to put the domain models directly into the ORM or whether you're mapping them to a persistence model and then putting that into storage is an implementation detail (private to the persistence layer) which does not affect how the domain itself is modeled.
Ideally I just want to be able to update my aggregate and save it through my repository.
Let's start higher level.
When dealing with a domain-centric design (I use this terminology to be slightly broader than just DDD, but it does fully include DDD), your domain is designed to be the lingua franca of your codebase. What that means is that when your internal layers (application, domain, persistence) speak to one another, the data objects they use as part of that communication should be domain concepts.
If you have a Person
aggregate root, regardless of how many subaggregates it may have, and regardless of how many tables you need to store that aggregate root's data in, the interface with your persistence layer should be talking about Person
, nothing more granular than that. Anything more granular has to be handled within a layer, without it being publically visible.
Let's use an example here of a Person
aggregate root, notably with some basic data fields and a profile picture, and there's an Address
subaggregate involved as part of Person
. Something along the lines of:
public class Person
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string AvatarBase64 { get; set; }
public Address ResidentialAddress { get; set; }
public Address PostalAddress { get; set; }
}
There are two classes involved here, but referring back to my earlier point, that granularity is not visible in communication between layers. Therefore, your interaction with the persistence layer (from outside of the persistence layer) should be along the lines of:
public interface IPersonRepository
{
public void Save(Person p);
public Person GetById(int personId);
}
Now let's focus on the persistence internals. For storage reasons, you actually want to break this down in three different storage methods: A People
table for the person's information, an Addresses
table to store all addresses, and a dedicated CDN for the profile picture.
Internally, your persistence layer can break this Person
down into smaller parts for the purpose of storing/retrieving the data. The only thing you have to keep in mind is that these deconstructed repositories are never used publically (i.e. from outside of the persistence layer), they should remain a private implementation detail of the persistence layer.
The implementation of IPersonRepository
could be something like this:
public class PersonRepository : IPersonRepository
{
// Omitted the injected dependencies for brevity
public void Save(Person p)
{
var personDetails = new PersonDetails(p.Id, p.FirstName, p.LastName);
_personDetailRepository.Save(personDetails);
_addressRepository.Save(p.Id, AddressType.Residential, p.ResidentialAddress);
_addressRepository.Save(p.Id, AddressType.Postal, p.PostalAddress);
_profileBlobStore.Save(p.Id, p.AvatarBase64);
}
public Person GetById(int personId)
{
var personDetails = _personDetailRepository.GetById(personId);
var residentialAddress = _addressRepository.Get(personId, AddressType.Residential);
var postalAddress = _addressRepository.Get(personId, AddressType.Postal);
var avatarBase64 = _profileBlobStore.Get(personId);
return new Person()
{
Id = personId,
FirstName = personDetails.FirstName,
LastName = personDetails.LastName,
ResidentialAddress = residentialAddress,
PostalAddress = postalAddress,
AvatarBase64 = avatarBase64
};
}
}
The interfaces and implementations of _personDetailRepository
, _addressRepository
and _profileBlobStore
do not need to be visible to anyone outside of the persistence layer, because no one except the persistence layer should ever interact with them directly.
Instead, the PersonRepository
is responsible for handling the incoming/outgoing deconstruction of the Person
domain model, so the communication with the persistence layer (from outside of it) can center around the Person
aggregate root.
Let's explore a second possibility for the persistence layer implementation. Suppose you have an ORM which is actually able to process the Person
object in its entirety. This may be the case because you have an advanced ORM, or because you are only using a plain document blob store that requires no specific handling for specific types. In such a case, you would do something along the lines of:
public class PersonRepository : IPersonRepository
{
// Omitted the injected dependencies for brevity
public void Save(Person p)
{
_myORM.Save<Person>(p.Id, p);
}
public Person GetById(int personId)
{
return _myORM.Get<Person>(personId);
}
}
The important take away here is that between these two hypothetical scenarios, only the concrete implementation of the persistence layer changed. The domain was never designed with either option in mind, and does not need to be altered based on any persistence-level design decision.
That is the core premise of the advice that led you to post this question.
Note
I am aware that your question centered around only updating what has changed. I omitted it from the example because there are many different ways of doing this and it would distract from the underlying layer separation that lies at the root of your question which I'm focusing on.
Diffing the current person state and the new person state is a responsibility of the persistence layer (since you're inherently comparing with the stored state), which means that this logic should live within the persistence layer, without the domain models or persistence layer interface being designed around it.
Second note
This is just me pre-empting a very common response to this design: comparing the entire Person
when saving, as opposed to actively tracking what changes have been made and directing the persistence layer specifically to perform those operations; indeed does not yield the highest runtime performance possible.
DDD does not prioritize runtime performance, it prioritizes clean layer separation and logical ownership, with the main goal being clean, readable and maintanable codebases. Pure DDD will generally not focus on runtime performance optimizations.
Pragmatically, you rarely want to do pure DDD for that precise reason. Real-world application design generally warrants some performance optimizations that DDD simply does not account for in its theoretical model, and this is the part where you need to start making compromises to your design in a way that gives you the needed optimizations without trading away the cleanliness and maintainability of the code. That is not an easy task and not something an internet stranger can solve for you - it requires a lot of context from your business and dev culture to make the right call.
The only way I found practical is by having aggregate data be duplicated, one is original props, second is current, like
type UserProps {
id: number
name: string
}
type User {
original: UserProps,
current: UserProps
}
When working with the aggregate we change current:
user.current.name = "new_name"
When saving to the database we just check what changed and generate the optimal sql query:
function save(user: User) {
let query = orm.user.update().where(id, user.current.id)
if (user.current.name != user.original.name) {
query.update('name', user.current.name)
}
}
save(user)
Explore related questions
See similar questions with these tags.