Eyes
x screenshot

A draughts example app

To illustrate the theory I explained in my earlier blogpost about Code architecture and domain-driven design (please read that one first), let's design an application here to see how it goes. The example we'll use is a website where you can play the board game draughts (sometimes also called checkers) against your friends. I chose this because it's both complicated enough that you need some structure and simple enough to use as an example.

The domain model

We start by designing our domain model. We'll just brainstorm all the classes we think we need for a game of draughts and then group them together in logical aggregates. [1]As a reminder:
- Entities are objects with an identity, if you change something, it's still the same object (example: you; you've changed a lot since you're born but you're still considered the same person).
- Value objects are objects defined by their values and thus are immutable, they usually belong to an entity (example: your email address, if it changes, it's considered a different one).
- Aggregates are groups of entities and value objects that together form a unit.
As a blog isn't the best medium to do that together I've just drawn out the result of my own brainstorm below.

The three aggregates that make up the draughts domain model
The three aggregates that make up the draughts domain model.
Entities are shown as circles, value objects as squares.

An interesting thing to note is that I put the classes for the game and its state in two different aggregates. This is because you often need the game without needing its state.
Another interesting thing is that I chose my board class to represent a 'position' (which is a value object) instead of a 'real board' (which would be an entity). Both are valid choices, I liked this one. [2]There is no objective to model your classes to the real world. They should be modelled to what they are used for.

So now we have an idea of what our domain model is going to be like on paper, let's look at how we would implement it in code. [3]These classes are simplified of course. You can see a full and working domain model on github if you want to.

class GameState : Entity {
  long Id { get; }
  private List<Move> _moves;
  int? CaptureFrom {
    get; private set; }
  BoardPosition Board { get; }

  GameState(long id,
      List<Move> moves,
      int? captureFrom) {
    Id = id;
    _moves = moves.Clone();
    CaptureFrom = captureFrom;
    Board = BuildBoard(moves);
  }

  void AddMove(int from, int to) {
    // Game logic here...
  }
}
class BoardPosition : ValueObject {
  private Piece[] _pieces;
  Piece this[int squareId] {
    // Draughts uses 1-based numbers
    // instead of coordinates
    get => _pieces[squareId - 1];
  }

  BoardPosition(Piece[] pieces) {
    // Clone to ensure immutability
    _pieces = pieces.Clone();
  }

  BoardPosition PerformMove(
      int from, int to) {
    // Game logic here...
  }
}
class Move : ValueObject {
  int From { get; }
  int To { get; }

  Move(int from, int to) {
    From = from;
    To = to;
  }
}
class Piece : ValueObject {
  Color? PlayerColor { get; }
  bool IsKing { get; }

  bool IsEmpty => PlayerColor is null;

  Piece(Color? color, bool isKing) {
    PlayerColor = color;
    IsKing = isKing;
  }

  Piece Promoted() {
    return new Piece(PlayerColor, true);
  }
}
The code for the classes in the GameState aggregate.

Note that classes are quite protective. They are responsible to make sure they are always valid, and therefore ensure outsiders can't mess with their fields. [4]Actually, in these examples they aren't protective enough - they lack validation in their constructors for example.

The domain service layer

To make a move on the draughts board, we need logic that involves both the game and its state. But aggregates cannot call other aggregates, and the application layer can't do any (domain) logic at all. So we need a domain service here. It'd look something like this. [5]Here's a full version on github.

class PlayGameDomainService {
  void DoMove(Game game, GameState gameState,
        long currentUserId, int from, int to) {
    game.ValidateCanTakeTurn(currentUserId);
    gameState.AddMove(from, to);
    game.NextTurn(currentUserId);
  }
}
The code for a domain service that combines two aggregates.

The application context

We now have a domain model that can do all the logic, but we don't actually do anything yet. Let's add a database and a controller, so that we can interact with the real world. [6]Full versions of a controller, a repository and its base class are on github.

class GameController : Controller {
  private PlayGameDomainService _playGameService;

  GameController(PlayGameDomainService playGameService) {
    _playGameService = playGameService;
  }

  [HttpPost("/game/{gameId:long}/move"), Requires(Permissions.PLAY_GAME)]
  IActionResult DoMove(long gameId, [FromBody] MoveRequest? request) {
    var game = _gameRepository.FindById(gameId);
    var gameState = _gameStateRepository.FindById(gameId);
    _playGameService.DoMove(game, gameState,
        base.CurrentUserId, request.From, request.To);

    return Ok(new GameStateDto(gameState));
  }

  record MoveRequest(int? From, int? To);
}
class GameStateRepository {
  GameState FindById(long gameId) {
    var dbGameState = SqlQueryBuilder.Init()
        .SelectAllFrom("gamestates")
        .Where("game_id").Is(gameId)
        .Single<DbGameState>();
    var dbMoves = SqlQueryBuilder.Init()
        .SelectAllFrom("moves")
        .Where("game_id").Is(gameId)
        .List<DbMove>();

    var moves = dbMoves.Select(m => new Move(m.From, m.To)).ToList();
    return new GameState(gameId, moves, dbGameState.CaptureFrom);
  }
}
The code for a controller and a repository.

Wrap up

We've seen some concrete classes to match the ideas from my previous blogpos. We don't have a frontend yet, but since I only wanted to design an API, I'll leave that as an exercise to the reader :).

Related blogposts