Reputation: 1342
Consider the following business requirements:
We have players which can play games. A player can play only one game at a time. A game needs two players.
The system will contain millions of players, and games take about two minutes. Concurrency issues are likely to emerge.
We want to comply to the rule that a single transaction involves a single aggregate. Further, eventual consistency must not lead to accepted games which must be cancelled afterwards (even if a short period of time) due to concurrency issues. Thus, eventual consistency is not really appropriate.
How do we need to define the aggregates and their boundaries to enforce these business rules?
I conceived two approaches:
1. Event-based handshake
Aggregate Player
, aggregate Game
.
When a game is requested, it pushed a GameRequested
-event. The Player
s subscribe this event and respond with a corresponding event, either GamePlayerAccepted
or GamePlayerRejected
. Only if both Player
s have accepted, the Game
starts (GameStarted
).
Pros:
Player
is responsible for managing his own availability which corresponds to the domain modelCons:
Game
is scattered throughout multiple aggregates (it seems like "fake"-eventual-consistency)Player
s if something went wrong2. Collection-aggregate
Aggregate Player
, aggregate GamesManager
(with a collection of value-objects ActiveGamePlayers
), aggregate Game
.
The GameManager
is requested to start a new Game
with two given Player
s. The GameManager
is able to ensure that a Player
only plays once at a time since it's a single aggregate.
Pros:
GamePlayerAccepted
, GamePlayerRejected
and so forthCons:
Player
to manage availability shiftedGameManager
is created and introduce domain-mechanisms which let the client not worry about the intermediary-aggregateGame
-starts disrupt each other because the GameManager
-aggregate locks itselfGameManager
-aggregate collects all active game players which will be tens of millionsIt seems like none of these approaches are appropriate to solve the problem. I don't know how to set the boundaries to ensure both strict consistency and clarity of the model, and performance.
Upvotes: 0
Views: 150
Reputation: 17713
I would go with the event-based handshake and this is how I would implement:
From what I understand you would need a Game
process implemented as a Saga
. You will also have to define a Player
aggregate, a RequestGame
command, a GameRequested
event, a GameAccepted
event, a GameRejected
event, a MarkGameAsAccepted
command, a MarkGameAsRejected
command, a GameStarted
event and a GameFailed
event.
So, when the Player A
want's to play a game with Player B
, Player A
receives the RequestGame
command. If this player is playing something else then a PlayerAlreadyPlaysAGame
exception is thrown, otherwise it raises the GameRequested
event and update it's internal state as playing
.
The Game
saga catches the GameRequested
event and send the RequestGame
command to the Player B
aggregate (this is a Player
aggregate with ID
equal to A
). Then:
If the Player B
is playing another game (it knows this by querying its internal playing
state) then it raises the GameRejected
event; the Game
saga catches this event and send a MarkGameAsRejected
command to Player A
; then Player A
raises the GameFailed
event and updates its internal state as not_playing
.
If the Player B
is not playing another game then it raises the GameAccepted
event; the Game
saga catches this event and send the MarkGameAsAccepted
command to Player A
aggregate; Player A
then emits the GameStarted
event and update its internal state as playing
.
In order to understand this you should try to model the use-case as if no computers would exist and players would be humans that communicate over printed mail.
This solution is scalable and I understand that this is required.
The other solution doesn't seem doable for milion of players.
A third solution would be to use a colection of active players in a SQL table or a NoSQL colection, without using the Aggregate tactical pattern. For concurency, when setting a pair of players as active, you could use optimistick locking or transactions where supported (low scalable) or two-phase commits (kind of ugly).
Upvotes: 2