1

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):

  1. 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)
    
  2. 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.

1

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:

  • Print
  • 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

| improve this answer | |
  • 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 Apr 4 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 Apr 4 at 18:35
  • But the invoice object would have to be an entity as well right? since it is the Aggregate Root (AR) of the Invoice? – ExcellentAverage Apr 4 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 Apr 4 at 18:52
1

Ideally I just want to be able to update my aggregate and save it through my repository.

  1. 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.

| improve this answer | |
  • 1
    I'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. – king-side-slide May 5 at 16:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.