Tactical Design by Example - Using Kotlin and Spring Boot (Part 12) - Using Kotlin Delegates for Domain Events

Let's talk a little more about behavior vs data today. Imagine modelling a chess game, especially the part where a player makes a move.

Behaviour

This would be a simple model for making a move that could come up in an Event Storming. The command is the blue sticky, the aggregate is yellow, and the events are orange.

chess

When you look at it from a behaviour perspective, there are two different outcomes of the "Move piece" command - either the move is legal, and a piece is moved, or it is illegal, and the piece is not moved. In code, this could look something like this

sealed class PieceMovedOrNot

class PieceMoved(
    val move: Move,
    val chessGame: ChessGame
): PieceMovedOrNot()

class PieceNotMoved(
    val move: Move,
    val chessGame: ChessGame,
    val reason: String
) : PieceMovedOrNot()

fun ChessGame.movePiece(move: Move): PieceMovedOrNot = ...

The advantage here is, that you don't have to look at the data in order to find out if and how the data changed. In Kotlin, the when function is helpful here.

when(val event = chessGame.movePiece(move)) {
    is PieceMoved -> ...
    is PieceNotMoved -> ...
}

Data

But when you look at it from the data perspective, the result of moving a piece in a chess game might be a modified chess game

class ChessGame {
    fun movePiece(move: Move): ChessGame = ...
}

The upside here is that you can easily chain calls to the model. Something like this would be great for testing:

chessGame
    .movePiece(Move(e2,e4))
    .movePiece(Move(e7,e6))
    ...

I think a data-centric approach is generally more popular. But is there a clever way we can introduce a behaviour-centric view, without losing the benefits of the data-centric view?

Implementation by delegation

Kotlin has this cool feature, where you can easily compose an object from various sources. It's called implementation by delegation, but the pattern is also known as mix-in. Now we can have the best of both worlds.

interface ChessGame {
    fun movePiece(move: ValidMove): PieceMovedOrNot
}

class PieceMoved(
    val move: ValidMove,
    val chessGame: ChessGame
) : PieceMovedOrNot(), ChessGame by chessGame

Now you can use the event just like you would use the data! Both the when-statement and the chaining would work.

Instantiation

A disadvantage here is, now that the name ChessGame is taken by the interface, we need to rename the class. Here, I've renamed it to ChessGameAggregate.

class ChessGameAggregate: ChessGame {
    override fun movePiece(move: Move): ChessGame = ...
}

Instantiating the object would look a little ugly, so I like to use a simple factory method.

fun chessGame(): ChessGame = ChessGameAggregate()

This is something that Kotlin often does, think of factories like setOf(). With the factory in place, there is no need to use the ChessGameAggregate constructor anymore.

Summary

I think there is real value in modeling domain events in code. Sometimes it's hard to see what's going on when all you have is data. Using events together with when-statements makes code legible, almost as good as plain english.

This concludes the series of blog posts on tactical design with Kotlin, I've learned a lot during the last months. I've also been busy creating a two-day training that contains both the modeling part (Event Storming on different flight levels), as well as implementation (using Hexagon Architecture and Kotlin features). I've created an elaborate Miro board and prepared a software project, so the training is really remote-friendly. If you or your whole team is interested in joining a training, feel free to contact me.