Reputation: 1117
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.
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.
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.
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.
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
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:
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