9

I am developing a large software project using DDD (Domain-Driven Design). I have an entity that acts as an aggregate root, let's call it Root. This entity refers to a collection of child entities of the type Child.

Because the use of this Child entity is an implementation detail that does not matter to clients of my Root entity, those children are solely managed by the Root entity, meaning the creation of new children and the removal of existing children all takes place in according methods of the Root entity. Pseudocode could look the following

class Root 
{
    private Collection<Child> children;

    public void addChild(ChildParameters params) 
    {
        this.children.append(new Child(params));
    }

    public void removeChildByCriteria(Criteria removalCriteria)
    {
       Child foundChild = this.children.findByCriteria(removalCriteria);
       if (foundChild) {
           this.children.remove(foundChild);
       }
    }
}

I think this is a very clean approach if you only look at the object model. However, this has to be persisted somewhere, so a Repository takes care of storing a Root entity and its children in a database.

I am struggling now with finding the right place for the logic to remove a persisted child from the repository (and therefore the database). Is it ok to let the implementation of the repository find orphaned children and remove them automatically? Or is the deletion of orphaned children domain logic that should be explicitly coded inside the root entity by passing a repository to the removeChildByCriteria method like

// ...
public void removeChildByCriteria(Criteria removalCriteria, ChildRepositoryInterface childRepository) 
{
    Child foundChild = this.children.findByCriteria(removalCriteria);
    if (foundChild) {
        this.children.remove(foundChild);
        childRepository.remove(foundChild);
    }
}
// ...

Or is it all wrong to place the logic to manage the hierarchy inside the root entity and instead a domain service or domain events and according listeners should be used?

3 Answers 3

8

You should review Udi Dahan's arguments in Don't Delete - Just Don't.

Of course, since then GDPR has become a thing. So you may want to include Forget Me as a domain concept.

Is it ok to let the implementation of the repository find orphaned children and remove them automatically?

That seems perfectly acceptable to me.

Evans, in the blue book, writes

A delete operation must remove everything within the AGGREGATE boundary at once.

I'm a bit disappointed at the lack of attention that he pays to the topic in the book.

Evans describes REPOSITORY as an abstraction of an in memory collection. Fundamentally, a delete operation breaks the link between the key (identifier) and the aggregate root, which allows the aggregate root to eventually be garbage collected in a domain agnostic way.

If your backing store were actually a RDBMS, rather than an in memory collection, then a cascade delete is a pretty reasonable approximation. That could be implemented in the data store itself, or written into your repository/ORM logic.

But if your delete protocol requires domain specific actions -- sending messages to other systems, and so on, then you should probably have a method within the aggregate that expresses that logic. Transforming a representation of an active aggregate to a representation of an end-of-lifed aggregate is a domain model responsibility.

"Delete" could reasonably mean either or both of these actions, but the responsibilities for them are clearly separated.

EDIT

I missed this on the first pass

its child entities from their repository?

Child entities don't normally have a repository of their own. Once you have decided that an entity belongs to an aggregate, it is subordinate to the aggregate root. You would normally arrange that all of the entities in the aggregate are loaded by the repository used to load the root.

2

Your Root object must maintain its own consistency. ie remove children from its collection where required.

The repository is responsible for persisting Root objects. So when you add a Root to the repository and pull it out again, the children on the root you pull out should be the same as the children on the root you put in!

Now, its up to you to choose When you persist Roots. One approach is to inject the repository and update the root in it after every change. But in my view this is somewhat problematic.

You are better off persisting the root outside of its own logic, in the app or service which is using it. This gives you much more control over how you are using the object. ie

btn_loadDoc()
{
    this.doc = repo.getDoc(id)
}

btn_addParagraph()
{
    this.doc.paragraphs.add(p);
}

btn_abandonChanges()
{
    this.doc = repo.getDoc(id)
}

btn_save()
{
    repo.save(this.doc)
}

here you have the option of not saving your changes, where as if you had injected the repo and called it on paragraphs.add() you would have to undo the changes on the db

3
  • delete: this.doc = repo.getDoc(id)?
    – Worthy7
    May 13, 2021 at 7:57
  • In my understanding of DDD (which is fairly new), root aggregates are re-built from the persistence layer at each transaction. That is, if the user clicks the btn_addParagraph(), that will call DocChangesServices.AddParagraph, which (i) loads Root from repo.getDoc(id), then (ii) calls Root.AddParagraph (which does this.doc.paragraphs.add(p)), and then (iii) finishes with repo.save. In your example, you seem to imply the state is persisted in memory once the transaction is complete; am I missing something?
    – Ama
    Dec 12, 2021 at 19:19
  • I like the approach that it is better that the domain doesn't decide when to persist data, but when you tell that when to persist is better to be set in the service, do you mean domain service or the service that use the domain? I mean, if I need a service to do some domain logic, I am thinking that is not in this service in which decide where to persist, but it is the cosumer that use this service domain who should to decide when to persist. Is it correct? Aug 16, 2022 at 10:46
0

Is it ok to let the implementation of the repository find orphaned children and remove them automatically?

Yes, it is a common way to do it. In practice, it will often boil down to letting an ORM or delete cascade DB constraints do their magic.

From the consumer's perspective, after you called Repository.Delete(entity) you're not supposed to hold a reference to the entity any more. The consumer is not interested in knowing how child entities are disposed of.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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