0

I'm trying to figure out how to integrate one command that updates multiple Aggregates in different contexts in a video game. The components/considerations for my particular design are DDD, CQRS, Ports and Adapters, Event-Driven Architecture, Aggregate+Event Sourcing, and Entity-Component-System. It's a lot to manage, but there are a few cases that can simplify quite a number of the friction points.

diagram showing interaction between purchase item command and inventory, shop, and wallet contexts

This diagram kind of illustrates what I'm working with. I have 5 different models, Inventory, Item, Shop, Currency, and Wallet. This is a collaborative, disconnected environment. What I mean by that is, each of these models is designed to not necessarily know about each other, except when necessary. I want these models to be reusable, by unknown third-parties, resulting in other compositions, aside from the ones that I can think of, or the one I'll talk about here.

For instance, the Item model doesn't know about the Currency model, or the Wallet model, or the Shop model, or the Inventory model. But, the Inventory deals with storing Items, so it knows about Items. The Shop model knows about assigning prices to Items, so it knows about Items and Currencies. The Wallet model knows about storing and keeping track of money, so it knows about Currencies.

Items can be added to a specific Inventory by using an AddItemToInventory command. Money can be removed from a specific Wallet by using a RemoveMoneyFromWallet command.

When a Player wants to buy an Item from a Shop, they select the given Item, and if they have enough of the specified type of money in their Wallet, they confirm they want to buy it. At that point, the money is removed from their Wallet, and the specified Item is added to their Inventory.

Because this is in a video game, I don't want to create places where duplication can occur. There are two particular cases I want to avoid.

  • An Item is added to a Player's Inventory, but money is not removed from their Wallet, allowing them to get the Item for free.
  • Money is removed from a Player's Wallet, but the Item is not added to their Inventory, resulting in lost money.

I've been trying to figure out how I can avoid these particular issues. My initial thinking was that this would be managed in a single transaction. The problem though is that these are in different models, different contexts, different Aggregates. Because they are different Aggregates, they shouldn't be updated in the same transaction. Other Aggregates could be updated asynchronously, in my case via published Domain Events. But because they're updated asynchronously, that still leaves open the problems, where items are given, but money isn't taken, or money is taken, and items aren't given.

In the diagram, I have the PurchaseItem command going into a ??? component. I don't know what it's supposed to be. I didn't want it to be given to the Shop Aggregate, because then the Shop context would have to know about Inventories and Wallets. It knows about Items and Currencies, but it feels improper for it to also know about Inventories and Wallets. The Shop should work in exactly the same way, whether the Item is added to an Inventory, and funds removed from a Wallet; or if the Item is added to a Package, and funds removed in one batch at the end of the day from a Customer Account, or any other combination of things.

So more than one Aggregate can't be updated at a time. I'm thinking that either another Aggregate needs to exist somewhere else, which becomes responsible for this interaction. This though would move the Source of Truth to this new Aggregate, making these targeted contexts, sometimes the Source of Truth, sometimes not. More like Eventual Source of Truth.

Or, maybe it's acceptable to create a new Aggregate, in a different context, which uses the same Entities and Value Objects of these other contexts. But these other contexts would still own their own data, so it would still be Eventually Consistent. I read in Implementing Domain-Driven Design about a bargain basement Domain Model, where three separate models were used, and a question was posed about whether it makes sense to introduce a new model for the interaction. My problem with that approach is that I still need these other contexts to exist as they are, because third-party developers will use them for completely different purposes. Using Items for Equipment, Money for Quest Rewards, In-game and out-of-game Shops. So I can't just condense these all into a single model, without also forcing those developers to NOT use most of the model that's available.

Additionally, all Aggregates in my design are sourced from Events. On the development side, the game developer will create a new Item by assigning a new Item Id. This gets recorded in the game files as an ItemIdAssigned event. Same thing for Currency, Shop, Wallet, Inventory, etc. But once these things are created on the developer side, they're interacted with in completely different ways by the Player. A developer can add an Item to a Shop, a Player can't. A Player can buy an Item from a Shop, a developer can't. This also indicates to me that there are different Aggregates that are at play here that I can't identify. Ones that may be sourced from all, or part, of a different Aggregate's Event Stream. Or that these Aggregates are missing functionality that I haven't been able to understand it's place.

How can I model this?

7
  • 3
    The internet is very confused about DDD and I think if you weren't trying to follow "DDD design principles" and various "rules" you can find there, you'd end up with code that's more aligned with DDD philosophy. For example, you said "because they are different Aggregates, they shouldn't be updated in the same transaction", but that's kind of backwards - instead, you'd spot some business behavior that would be problematic if updated in the same transaction, and then you'd change the design so that you organize your domain objects into separate aggregates to support that. 1/2 Feb 24, 2021 at 17:16
  • 3
    Also, what you call bounded context aren't bounded contexts as defined in DDD. This is all a single bounded context; and your "contexts" are just logical partitions within that. So don't worry about any aspects of DDD that have to do with different bounded context, because you don't (and most likely shouldn't have) anything remotely like that here. I feel like you're trying to shoehorn the needs of your game into your initial idea of what the design should be (so that it adheres to the "rules of DDD"), rather then come up with a design that serves those needs. 2/2 Feb 24, 2021 at 17:16
  • Is this a hobby project ? Feb 24, 2021 at 17:50
  • @AlexanderPetrov That's a tough question. I'm trying to develop a new game engine, that supports a highly collaborative, extremely flexible, third-party modifications, permitting developers and users to create very detailed experiences without the requirement of knowing how to program, or develop games. My goal is to provide off-the-shelf components that users can drag-and-drop to create the game they want to play. Many games have Items, many games have Shops, many games have a Wallet of some kind. I want as close as possible to be able to click-and-drag the Shop JAR, the Item JAR, and the...
    – Zymus
    Feb 24, 2021 at 18:25
  • 1
    @Zymus not an answer, but piece of advice. I have been in your shoes building product from scratch, being only developer. Focus on one only thing - minimum viable product. As single developer your resources are very limited. You should not spend even a second than nessesary on technicalities. Once you have something working and earning money do whatever you want technicaly. Feb 24, 2021 at 23:05

3 Answers 3

4

This answer is based on the presumtion that this is not a hobby project: Probably that is not what you want to hear , but trying to go at the same time DDD, CQRS, Event Driven Architecture, Event sourcing sounds like a bit of big bite that will lead, either to nowhere, or to a very overengeneered system. If it is a hobby project put it in the description so I will try to answer it. Otherwise my answer is re-think the architecture to have less amount of Buzz words.

Pick some concepts of these architectures or just pick one of them and not try to build a system only to test these architectures.

I was part once of a project that was trying to utilize at the same time: TDD,CQRS,Event Driven architecture,Behaviour testing and Domain Driven Design and microservice architecture. A project that normaly should take months was year and half in development without visible end because of the added complexity of the different architecture.

I find CQRS one of the main sources of overengeneering. I think majority of developers understand very literaly that there MUST BE one read store and one write store which raises multiple problems related to synchronization of two stores. While I have personaly thought that the main principle of CQRS is that you have two different channels and if you actualy have 1 read and 1 write stor or read and write store are the same. As long as you have 2 separate channels all is good.

1

There is good news, bad news, and more good news.

The good news is that this is a solved problem! You have found yourself in the exact space that every ATM in the world needs to navigate before dispensing money.

The bad news is that the infrastructure and complexity necessary to mediate this kind of distributed transaction is considerable (and not perfect!). This is a pretty good summary of how they work: How are transactions on ATMs and their banks typically synchronized?

The final bit of good news is that you do not need to go the above route (and if/when you do reach that point, it will be a good problem to have). How much easier would it be if you just locked and updated your data in a single transaction? I urge you to make a list of the pros and cons. You will find that the only real issue with a single transaction is that it limits availability. Are you having problems with availability?

DDD has nothing to say about database transactions. The boundaries around aggregates are logical boundaries. This is often a point of confusion. DDD doesn't mandate eventual consistency.

1

Create a new entity called "Transaction" or "Purchase." Each instance of this entity records the event when a user requested and completed a purchase (Note that "requested" and "completed" are not the same thing).

The workflow goes like this:

  1. User requests a purchase

  2. System create a purchase record with an identifier specifying which item was requested for purchase

  3. The purchase record contains blank slots to store

    (a) The identifier that is created as a side effect of collecting payment (e.g. a confirmation code or transaction ID). This is left blank until payment is actually collected.

    (b) An identifier that specifies which item was moved to the user's inventory. This is left blank until the item is actually moved.

  4. After all identifiers are populated, the purchase record is marked as "completed."

At any given moment, if your system is not certain of the integrity of a transaction, it can go back and look at the record. If the payment identifier is blank, it knows to go back and collect payment. And if the item identifier is blank, it knows to go back and move the item to the user's inventory.

This new entity must know about items and payments (obviously), but items and payments don't need to know about the new entity, preserving your loosely-coupled design.

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.