shawnseanshaun
shawnseanshaun

Reputation: 1117

DDD: How to model an operation which affects two aggregates?

The domain:

I have an InventoryItem aggregate and a Kit aggregate. An InventoryItem may be added to one (and only one) Kit. An InventoryItem has a Type property, which is an enum of various item types the business uses. A Kit will have multiple InventoryItems.

I am using event sourcing. Each InventoryItem aggregate is stored in its own event stream, and each Kit aggregate is stored in its own event stream.

The problem:

An InventoryItem must know whether it is part of a Kit in order to enforce invariants such as blocking deletion if it is, and not allowing it to be added to another Kit.

A Kit must know how many and what type of InventoryItems have been added to it in order to enforce invariants such as not allowing shipment until the Kit requirements are completely satisfied.

However, one transaction is supposed to affect only one aggregate. So, when an InventoryItem is added to a Kit, I'm unable to use one transaction to update both the Kit and the InventoryItem with the required information. I've been thinking through several solutions, and each one seems to present several issues.

Using a process manager/saga to model the operation as a long-running process.

This feels wrong to me, as the operation is really not a long-running process. It also runs the risk of a stage failing and requiring rollback of the previous stage. All in all, it feels like a lot of overhead for something that should be a simple operation.

Using domain events to update either the Kit or InventoryItem.

I could update one of the aggregates with the initial transaction, then dispatch a domain event to update the second aggregate. However, this runs the risk of an out-of-sync domain model if for some reason the dispatched event fails. Such a case would also introduce the possibility of another user adding the InventoryItem to another Kit before the out-of-sync model is noticed and fixed. Once again, it feels like there are a lot of potential problems and complexity for what is a very simple operation.

Creating another aggregate.

I've considered introducing another aggregate such as KitItem. This aggregate would essentially store the Kit/Inventory Item relationship, and the Kit and InventoryItem aggregates would hold references to it. This seems to me to be the most reasonable approach, and yet it once again introduces the possibility of an out-of-sync model. For example, what if a Kit is deleted? We could use a domain event to delete all KitItems with a relationship to the Kit, but what if that fails for some reason? Additionally, if the Kit is only allowed to hold a reference to the KitItem, how would the Kit know what types of InventoryItems have been added to it?

Does anyone have any advice on what approach to take here, or if there's another approach I haven't considered?

Upvotes: 0

Views: 816

Answers (1)

Reasurria
Reasurria

Reputation: 1858

I encounter this problem a lot at work. Normally I find that the operation you are defining constitutes or belongs to a different context. And normally the real aggregate root is something one level higher up in your hierarchy of entities. Not too different from your 3rd idea. I don't know how your persistence/events are set up but I think the solution should still apply.

For context and to make sure we understand DDD the same way, you can have a look at my answer here: Can aggregate root reference another root? I see you are concerned about referencing one root from another, which is why I think this is relevant. TL;DR - don't be afraid of modeling many different roots for many different contexts, even if they represent the same data. The root MUST have everything it needs to satisfy the context requirements, no more, no less.

Remember that one of the strengths of DDD is to simplify the problem to something easy. So instead of asking "how do I model something that modifies two aggregates", rather ask "why are there two aggregates" (maybe it's not feasible in your situation to have it any other way; that's a valid answer).

Imagine having a new bounded context with a new aggregate root. Let's call it Shop, because I have no idea what the rest of your domain is. Essentially it would be the entity that is one level up in your entity tree. For example, if we were working with Car (Kit) and Wheel (InventoryItem), we are looking for Factory.

Now say we redefine the aggregate itself as the following:

public class Shop : IAggregateRoot
{
    public ICollection<Kit> Kits {get;private set;}

    public DomainEvent AddItemToKit(int kitId, int itemId)
    {

        if(this.Kits.Any(k => k.HasItem(itemId)))
        {
            return ItemAddFailedEvent(kitId, itemId, "Item already in another kit"); 
        }

        return new ItemAddedEvent(kitId, itemId);
    }
}

public class Kit : IEntity
{
    public int Id {get;private set;}
    public ICollection<int> Items {get;private set;} // Just IDs

    public bool HasItem(int itemId)
    {
        return this.Items.Contains(itemId);
    }
}

By defining the root as something higher up in the relationship tree of your overall domain, you now have access to all the information you could want. It becomes trivial to manage the problem.

I've left out some details you have mentioned, such as identifying item types on a kit. I think it would be simple enough to add that onto this starting point. Just make InventioryItem an entity and map what you need.

Let's look at deletion:

public class Shop : IAggregateRoot
{
    public ICollection<Kit> Kits {get;private set;}

    public DomainEvent DeleteItem(int itemId)
    {

        if(this.Kits.Any(k => k.HasItem(itemId)))
        {
            return new ItemDeleteFailedEvent(kitId, itemId, "Item is in a kit"); 
        }


        return new ItemDeletedEvent(itemId);
    }
}

Again, the solution has become trivial. Given this root, it's simple to check if we can delete.

Referring back to my other answer linked above - this entire aggregate and all 3 of the example classes here would all be brand new. It doesn't matter if you already have a class called "Kit" somewhere in your system. This is a new context with a new problem. You don't need to risk breaking existing features to make this new feature work. Sure, there is overhead in mapping, but that's a price I've found is almost always worth paying.

So for you to get this to work using this approach, you need to:

  1. Make the new classes
  2. Map/load/hydrate the BARE MINIMUM properties needed. Can't stress this enough.

Of course reality is never so simple. There are countless variables that you know about your system that would take hours or days to explain in detail.

Almost always the problem is between the domain model and how the data is stored. Try to not let those 2 things interfere with each other.

Model the domain as pure as possible. This makes the solution simple.

Then model the data as efficient as possible. This makes the solution performant.

Then map and compromise until the solution exists.

I'm sure that if someone asked you to write the domain solution independent of any persistence mechanism, you would have nailed it by yourself, as the business problem is simple. Unfortunately we sometimes create technical problems by accident.

Upvotes: 1

Related Questions