Reputation: 1120
I would like to ask about a problem I have using DDD in the implementation of a game.
I have a situation where, based on a message the system receives, a new aggregate for a player is created. The problem comes from the fact this new aggregate has to be referenced by another aggregate (a table aggregate) using its id. This second aggregate keeps a value object with information about the player at the table like his position or colour chosen. The player aggregate stores players actions because, depending on the number of players and actions issued by them, the table aggregate could become massive and the lock contention at the database would be too costly if I keep everything on table. For the player to be able to join the table, some validations have to be passed first, like colour not taken, maximum number of players already reached or games that the player has been inactive, that's why this value object is stored at table with info from the players.
So, based on this, I only create the player aggregate when he issues the first action to be stored outside the table. The problem with that is that table could hold value objects referencing an aggregate that hasn't been created yet which makes the code look ugly as it forces it to check for null references. The other option, that is creating first the player aggregate, also breaks the design as the validations to create a player happens on the table first. Because of the concurrent nature of the system, validating first on table to then create the player aggregate and then, on receiving the event from the creation, update the table, could cause many race conditions. The only thing I can think of is updating two aggregates in the same transaction but that goes against DDD rules.
Is there any nice solution to this? I guess in the end this is going to be related to the design but I cannot think of any way it could work without introducing performance issues.
Thanks.
Upvotes: 3
Views: 3474
Reputation: 14064
As often, part of your issues may be solved by looking at them from a domain and ubiquitous language perspective.
Udi Dahan has an excellent article about how Aggregate Roots don't appear out of thin air but as a result of a domain action that must be a first class ubiquitous language citizen. In your case, Player could be created not by the application service but closest to the domain source, by the Table
AR.
Note that I don't necessarily advocate holding full references to other Roots like in Udi's example, but you could replace that with Table.ReceivePlayer(...)
returning the newly created player, which can then be added to PlayerRepository
by the application service. Since Player
doesn't play the role of an Aggregate Root (i.e. single entry point to modifications and invariant enforcement) but that of a simple Entity in that scenario, you could legitimately wrap all that in a single transaction.
The forces that drive Aggregate modelling are basically true invariants, concurrency and performance. To justify how you modelled yours, you state that the table aggregate could become massive and the lock contention at the database would be too costly if I keep everything on table.
I think it would pay off to explore a bit more what this contention would really be and putting domain terms behind it, enriching your ubiquitous language. Is a table massively concurrent ? How so ? Does the game have mutable shared state that players modify ? Do players need to make decisions based on other players' moves, introducing some kind of bias if concurrent actions are taken in the mean time ? How does all that translate in domain terms ? What does the additional Player
aggregate bring you ?
In terms of performance, what would a massive Table
aggregate look like and how would it affect the system ?
The "no transaction spanning multiple aggregates" rule is just a corollary of approaching Aggregates as consistency boundaries, it's not set in stone. It doesn't make sense to craft small, independently consistent aggregates and pack a ton of modifications to multiple of them in a single transaction, because you create unneeded contention, and potential concurrency issues ensue. But in a scenario where no concurrency problems exist beyond those of the sole Table
AR, and this is certainly the case with creation since no-one else is aware of the Player being added, it's just as if you had only modified one Aggregate and the rule doesn't really hold any more.
If you respect the rule in the 99% modification cases but don't in the 1% creation cases, you're still in the lines.
Upvotes: 3