1

Reading Domain-Driven Design, on the section of Aggregates, it says

The root is the only member of the Aggregate that outside objects are allowed to hold references to, although objects within the boundary may hold references to each other.

The Application Layer is the direct client of the domain model.

Can objects outside the Application Layer hold references to Aggregate Roots, or are the outside objects limited to the Application Layer objects? For instance, in another model or context?


From the comments, I suppose I'm asking about both separate layers, and separate models (different application in different domain).

Let's say I had a Shop Aggregate, which maintained a list of Items, with an associated Price. When a Player purchases an Item from a Shop, the cost of the Item is deducted from their Wallet, and the Item is added to their Inventory. These are separate Models, and they don't necessarily know about each other individually; ie. the Wallet doesn't know about Shops, the Items don't know about Wallets, etc. But I need to perform this coordination somehow. My ultimate goal is to perform this via Domain Events, but that's a separate question. This coordinating component, within a single transaction, would deduct the price of the Item from the Player's Wallet, and add the specified Item to their Inventory. The reason I want this to be as transactional as possible, is because this exists within the context of a video game, so I want to prevent annoying or exploitative results (like having the money removed from the wallet, but the item not being added; or the item being added, but the money not being removed, allowing duplication of Items, or Money).

As for the separate layer portion, the implementation of an Event Store, or Repository, would exist in the infrastructure layer (by Ports and Adapters).

While typing these portions of the comment response, I realized that it's a bit more obvious; why would I need to reference an Aggregate somewhere else, when everything I would want or need to do with that Aggregate should be exposed via Application Service, preferably referencing the Aggregate with an Id Value Object? I probably wouldn't need to reference the actual Aggregate, especially if I follow the rule of only modifying on Aggregate per transaction.

I would still like input as to the original quote, but I will write up another question for the portion I got confused about.

2
  • Can you perhaps give an example? It’s not clear to me if you are thinking about other layers (which ones) or if you mean in another model i.e. another application for a different domain. The arguments in the answer could be different.
    – Christophe
    Feb 23, 2021 at 12:20
  • @Christophe edited. I feel I'll need to write a separate question to get to the deeper issue.
    – Zymus
    Feb 23, 2021 at 13:25

1 Answer 1

2

The reason I want this to be as transactional as possible, is because this exists within the context of a video game, so I want to prevent annoying or exploitative results (like having the money removed from the wallet, but the item not being added; or the item being added, but the money not being removed, allowing duplication of Items, or Money).

Stick to command-driven architecture to ensure that outside actors cannot subdivide your commands into smaller operations. The specifics on how to implement this depends on how your system is structure. At a basic level, you'd do something like:

public class PurchaseGoodsCommand
{
    public int TotalPrice { get; set; }
    public Dictionary<GoodType, int> Goods { get; set; } // Key = item type, value = amount 
}

I am going to leave the handling logic up to you, essentially taking money from the wallet and adding items to the inventory.

However, this is exploitable if you don't validate the price of items. I could send a request to purchase anything I want for a price of 0 (or even negative!)

Can objects outside the Application Layer hold references to Aggregate Roots, or are the outside objects limited to the Application Layer objects?

Depending on how to validate the price, you can change this. If prices are fixed, then only pass the Goods and let the backend figure out the price. If prices can differ between shops, then you need to pass Goods and some sort of ShopId so the backend can figure out the correct price for the shop.

If you let the pass the Shop object themselves, instead of the ID, then you give them the freedom to construct their own shop, which then can set its own prices. By forcing the consumer to pass the ShopId, you force the consumer to refer to an existing shop which your backend manages.

Of course, this implies that your consumer isn't able to create new shops in your backend either.

Overall, it's easier to have your backend maintain the data store, and to force the consumer to use references to that data, rather than pass the data themselves.

Note the ambiguity of "reference" meaning object reference or resource identifier. In your question you meant the former, but I mean the latter here.

5
  • Thanks for the insight. I hadn't considered the option of specifying all parameters in the command. What I mean is, I had been leaving some properties up to be inferred from the Aggregate. For instance, the ItemAddedToShop command I have takes in the ShopId, the ItemId, CurrencyId, and Price. But my ItemRemovedFromShop command only had a ShopId and ItemId; not the CurrencyId, or Price. This does illustrate that a command may come in with incorrect assumptions; for instance, if an Item was priced with Dollars, but it was removed with Coins or something, can it execute the command? Do you have..
    – Zymus
    Feb 23, 2021 at 15:29
  • .. any insight as to how to determine which properties are suited for different commands like this? I've thought about mirroring them, where the properties necessary to add are also necessary to remove; but keep feeling that there may be cases where we simply want to remove it, regardless of what new state it may have taken. What indicators can I look for to help make a better decision?
    – Zymus
    Feb 23, 2021 at 15:34
  • @Zymus (1) ItemAddedToShop is not a command, it is an event. Or it's wrongly named. (2) "where the properties necessary to add are also necessary to remove" This doesn't make sense. I can request data needed for the creation of a resource (e.g. name), but the deletion only requires an identifier. I wouldn't add more to the delete command, for the exact issue you point out: it implies (or begs the question) that the behavior is different if a certain property value is different, which is not functionally relevant to a delete action.
    – Flater
    Feb 23, 2021 at 15:56
  • @Zymus: (2 continued) That being said, if you need an extra discriminator, then obviously you can add it. For example, only deleting the $2 potatoes from the shop and not the $1 ones. As you can see, the unit price is in this case part of the identifier that you need to recognize the correct item(s) to remove. But whether you need this or not is heavily contextual and I can't just answer this for every possible game structure out there. The average RPG has a very different shop system to an economy simulator, there is no one-size-fits-all answer here.
    – Flater
    Feb 23, 2021 at 16:03
  • Yes, apologies, stayed up later than I meant to. The command was AddItemToShop, which results in an ItemAddedToShop event. For this particular model, I don't need the extra discriminator, as each Item can only exist in a Shop once, with a specific price. So I wouldn't end up in a case where I have two Potion Items, sold for different prices. In a slightly different Player Shop model, Players can choose the Items they sell, and the prices to sell them for, so the extra discriminator is more useful there, for the exact reason you mentioned.
    – Zymus
    Feb 23, 2021 at 16:12

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.