Eduard Keilholz

Hi, my name is Eduard Keilholz. I'm a Microsoft developer working at 4DotNet in The Netherlands. I like to speak at conferences about all and nothing, mostly Azure (or other cloud) related topics.
LinkedIn | Twitter | Mastodon | Bsky


I received the Microsoft MVP Award for Azure

Eduard Keilholz
HexMaster's Blog
Some thoughts about software development, cloud, azure, ASP.NET Core and maybe a little bit more...

Building a Boardgame in Azure - Part 3 - Domain Model

Building a Boardgame in Azure

About DDD

I’m not a DDD guru, but there are absolute parts of DDD that I really love. One of them is the concept of domain models.

A domain model is a conceptual model of the domain that incorporates both behavior and data.

Now let’s take a step back there. This problem is, then when a software system becomes larger and larger, it becomes buggy, unreliable, and difficult to understand. Communication among teams becomes confusing. It is often unclear in what context a model should not be applied. In DDD, this is where the bounded context kicks in.

The bounded context describes the context within which a model applies. You should explicitly set boundaries in terms of team organization, usage within which part of the application, and physical manifestations such as code bases and persistence.

Thus the domain model lives in that bounded context. Team members use the same language (terms) called the ‘Ubiquitous Language’ within that model. This means that language may change throughout models. For example, when logging in to a system, the person logging in could be called an ‘Identity’. When writing a domain model for this game, that same entity in the ‘Game Model’ would be called a ‘Player’.

The model

Game Domain Model It’s important to understand that the Domain Model must always remain in a valid state. So the image shown here, called an ‘Aggregate’, has a game class (called the ‘Aggregate Root’). This ‘Aggregate Root’ forbids external objects to hols references to its members. Changes made to the model, are controlled and validated by the model. This forces developers to write all the functional ‘domain logic’ in that model, having it in a single place, which is a nice concept in my opinion.

Let’s start with properties. The Game class contains a couple of properties, for example, the Name. The name of a game can be changed over time, but the name cannot be null or have a length of 0. To make sure the Name property always contains a valid value, the Setter must be made private. A public method allowing you to set the name, allows you to validate the new value and throw exceptions in case the new value is invalid before the actual value of Name is changed.

public string Name { get; private set; }

public void SetName(string value){
    // Validation rules here
    Name = value;
}

A small side-note here, Yes! You can write these validations in the setter of the property. We can have a huge discussion about that, but I think property bodies should be lean and clean.

Now the reason why I think this solution is so nice and clean is that (I already mentioned) all the business rules are in the same place and the domain model always stays in a valid state. You can easily use unit tests to increase confidence the domain model always remains valid. This means that I can persist the model anytime I want because I’m pretty sure it’s valid.

In this case, I’m using SQL Server to persist the game model. The data model used for SQL Server doesn’t have to match my domain model. I can write a mapping between the model within the repository so I can optimize the data store as I like.

I like to mention that I make a difference between Commands and Queries. A little bit like CQRS, but different. My data repository will always return a domain model when I’m executing a command. So for example, when I want to update the Game model, I’ll fetch the current state from a repository, which returns a domain model. I’ll apply changes to that domain model and then persist the Game model again. For queries, I always return a DTO. At first (since I’m using SQL Server in this case) I will execute queries with joins and includes and all the fancy stuff. But when performance drops in later stages I can easily optimize performance by creating materialized views for example to query from.

And finally, because all my business rules are at the same place I can recognize changes more easily and act on those changes. DDD knows integration events and domain events. The difference between the two is described in detail in this blog post by Cesar De la Torre. Given the example of the Name property in my Game model, there is now one place and only one place where the Name property actually changes. So if I want to notify systems, I can raise domain events and or integration events also in one single place.

Business rules

Now we’ve got the basics of this domain model under control, it’s important to think of business rules that apply within our domain model. This way, we can implement actual game-play into the model.

When I have a couple of Friendships in the system, allowing me to create a new game, I can just select three Friends and go ahead and create a new game. My Friends agreed upon the Friendships, but didn’t agree on playing the game. So I came up with a business rule that Players in a game must accept that game instance by setting themselves to a Ready state. In other words, when a game starts, every player’s state is set to ‘Not Ready’ and they must set themselves to ‘Ready’ telling the system that they accept the game.

Now when all the players of a game are set to ready, the game can start. But with starting a game, a whole lot of things need to be done. And this is where our Domain Model turns out the be extremely handy!

Now take a look at the code below, I’ve removed some lines to make it more readable.

public void SetPlayerReady(Guid id, bool isReady)
{
    var player = Players.FirstOrDefault(x => x.Id == id);
    if (player == null)
    {
        throw new PlayerNotInGameException($"Player {id} is not a player of this game");
    }
    if (Status.Id != 0)
    {
        throw new GamePlayerException("Cannot set player ready state when the game state is not new", ErrorCode.GameAlreadyStarted);
    }

    player.SetReady(isReady);
    if (Players.All(x => x.IsReady) && Status.Id == GameStatus.New.Id)
    {
        Start();
    }
}
private void Start()
{
    if (Status.Id != GameStatus.New.Id)
    {
        throw new GameControlException("The game has already started", ErrorCode.GameAlreadyStarted);
    }

    if (Players.Any(x => !x.IsReady))
    {
        throw new GameControlException("Not all players are ready", ErrorCode.NotAllPlayersAreReady);
    }

    Status = GameStatus.Started;
    NewRound();
}
public void NewRound()
{
    if (CurrentRound != null && !CurrentRound.IsFinished)
    {
        throw new GameControlException("The previous round is not complete. Cannot start a new round", ErrorCode.PreviousRoundIsNotComplete);
    }

    SetNewDealer();
    DrawNewCards();
}
private void SetNewDealer()
{
    var currentDealer = Players.FirstOrDefault(x => x.IsDealer);
    var dealerSequence = currentDealer?.Sequence ?? (byte)Players.Count.GetRandom();
    dealerSequence++;
    if (dealerSequence >= Players.Count)
    {
        dealerSequence = 0;
    }

    foreach (var player in Players)
    {
        player.SetDealer(player.Sequence == dealerSequence);
    }
}
private void DrawNewCards()
{
    foreach (var p in Players)
    {
        p.DismissAllCards();
    }

    int cardsDrawn = 0;
    do
    {
        foreach (var player in Players)
        {
            player.DrawsCard(CurrentRound.Draw());
        }

        cardsDrawn++;
    } while (cardsDrawn < CurrentRound.CardsToDraw());

    SetPlayerTurn();
}
private void SetPlayerTurn()
{
    var currentDealer = Players.FirstOrDefault(x => x.IsDealer);
    if (currentDealer == null)
    {
        throw new GameControlException("No player was marked as being the dealer. Cannot determine which player has turn", ErrorCode.DealerNotFound);
    }

    var dealerSequence = currentDealer.Sequence + 1;
    if (dealerSequence >= Players.Count)
    {
        dealerSequence = 0;
    }
    foreach (var player in Players)
    {
        player.SetHasTurn(player.Sequence == dealerSequence);
    }
}

Now that’s some piece of code, but it indeed shows the power of a Domain Model. As soon as a Player sets it’s state to ready, the system checks if now all the players are set to ready. If so, the game can start.

Now you must know that for this game, an indefinite amount of rounds is played (until the game is finished). Each round, a dealer is assigned but for the first round, the dealer is selected randomly. When the dealer dealt the cards, the player next to the dealer will have a turn and can play the first card.

When a new round starts, all 52 playing cards of a deck are stored in that round.

As you read along with the code, this is exactly what happens in the domain model. When the game is started a new Round is created. The system will select a new Dealer or if there was no previous round (like in this case, the game is new) it will randomly select a new Dealer.

Now the cards are dealt. This means that the system randomly picks a couple of available cards from the Round and assigns this card to a player.

And finally, I pick the player next to the dealer and make sure this player has a turn.

All this is done because a Player sets its state to ready. An awful lot of things happened and the Game model changed dramatically. I can now persist the game (store it in SQL Server). And when using a Transaction, I make sure that either everything is stored, or now changes are made to persistence.

Also, I can detect when the current round ends, and now easily call the NewRound() method to create a new round, advance the dealer, draw new cards, and make sure the correct player has a turn.

Conclusion

Although I’m not a DDD expert, implementing some of the concepts definitely makes sense. It’s nice to start and play with it. And yes, you may run into problems or struggles like I did for example using Value Objects, but I think learning is fun. And sometimes you need to struggle a little bit to learn more. In the end, we all do…