Functional programming is a popular programming paradigm that lives side-by-side with object-oriented programming. Functions and immutable data structures are core concepts that play an important role in this blog post. In my previous post, I described how we can build an event-sourced domain model by applying domain-driven design (DDD) and its tactical patterns, of which many but not all are object-oriented. In this blog post, however, we will aim to build the same event-sourced domain model using a functional approach. It should be noted that the strategic patterns from DDD can still be used to elaborate a domain model, regardless of which implementation approach (e.g. DDD’s tactical patterns or functional approach) is used.

Recap on the domain

Let’s start by briefly describing the domain used in this blog post. Similar to the previous blog post, we are in the context of a library domain, in which you can borrow books, return books, make a reservation, and so on.

Once a book is registered, it is available for lending. A book can only be lent to one reader at a time. After a reader finishes reading the book, the book is returned and made available again. A book can only be reserved if it is currently borrowed. Reservations can be cleared regardless of whether the book is borrowed or not. Of course, there are way more states in a real application, but the model described will be sufficient for the purpose of this blog post.

Given the description above, we can visualize a book’s lifecycle using a state machine. The following state machine diagram illustrates the states and transitions of the book scenario provided. We will come back to the illustration as we start implementing the domain model.

State machine diagram for a book lifecycle

Events as the Source of Truth

In order to build an event-sourced domain model, we have to build the application state entirely from domain events. Luckily, the previous state machine diagram already contains the events for our book entity. Each transition is triggered by the occurrence of a specific event, that is, we can map each transition to a specific event type.

How does this look like in code? The following code snippets present code written in Kotlin, which comes with good support for functional programming. In the case you use a different language, don’t worry, the code should look more or less similar to other popular programming languages.

For each type of event, we define a data structure that contains all the necessary information about that specific event. We also define a sealed interface in order to be more explicit and group those events. The main advantage of a sealed interface is that we later can do an exhaustive pattern match, and there are no other events defined outside the module. Kotlin’s data class helps us to define immutable data structures.

sealed interface BookEvent

data class BookRegistered(
    val bookId: BookId,
): BookEvent

data class BookBorrowed(
    val bookId: BookId,
    val readerId: ReaderId,
    val loanId: LoanId,
    val loanDate: LocalDate,
    val loanEndDate: LocalDate
): BookEvent

data class ReservationCleared(
    val bookId: BookId
): BookEvent

data class BookReturned(
    val bookId: BookId,
    val loanId: LoanId,
    val returnDate: LocalDate
): BookEvent

data class BookReserved(
    val bookId: BookId,
    val readerId: ReaderId,
    val reservedAt: LocalDate,
    val expiresAt: LocalDate
): BookEvent

Now that we have defined all events, let’s explore how we can build the book entity from those events using a functional approach.

Building a Functional Domain Model

Functions and immutability do both match with event sourcing. Greg Young, who coined the notion of event sourcing, emphasized that event sourcing is a purely functional model. Hence, the motivation of this post is that we at least should consider embracing the idea of immutability in our event-sourced domain model.

The first step to implementing our event-sourced domain model is to define the book entity itself. Similar to the approach used for events, we can also use a data class with vals to ensure immutability, as shown in the following code snippet:

// THE BOOK ENTITY
data class Book(
    val bookId: BookId,
    val loan: Loan,
    val reservation: Reservation,
    val state: BookState,
) 

Loan and Reservation are two self-defined types for implementing the concept of a loan and reservation. The following code snippet shows the implementation of a Loan. The implementation also makes use of sealed interfaces, as a loan can be in different states. Furthermore, this approach avoids working with null values.

sealed interface Loan

object AvailableForLoan : Loan

data class ActiveLoan (
    val loanId: LoanId,
    val readerId: ReaderId,
    val startDate: LocalDate,
    val endDate: LocalDate,
    val extensions: Int
): Loan

The implementation is completely immutable, that is, for changing the state of a book, a new object has to be created. Kotlin’s data class comes with a copy() function that simplifies creating new objects from a previous state. You will see what this looks like in the next code snippet.

In order to build book entities solely from events, we define an apply() function that takes an event and the current state. The apply() function then builds the new current state of the book entity and returns it, as shown in the following code snippet.

data class Book(
    val bookId: BookId,
    val loan: Loan,
    val reservation: Reservation,
    val state: BookState,
) {

    companion object {

         // PLACEHOLDER FOR NON-INITIALIZED BOOKS
        fun empty(): Book = EMPTY_BOOK


        // Applies the specified event to the current state
        // of a book and returns that the new state.
        fun apply(event: BookEvent, current: Book): Book {
            return when (event) {
                is BookRegistered -> Book(
                    event.bookId,
                    AvailableForLoan,
                    NoReservation,
                    BookState.AVAILABLE
                )

                is BookBorrowed -> current.copy(
                    loan = ActiveLoan(
                        event.loanId,
                        event.readerId,
                        event.loanDate,
                        event.loanEndDate,
                        0
                    ),
                    reservation = NoReservation,
                    state = BookState.BORROWED
                )

                is BookReturned -> current.copy(
                    loan = AvailableForLoan,
                    state = BookState.AVAILABLE
                )

                is BookReserved -> current.copy(
                    reservation = ActiveReservation(
                        event.readerId,
                        event.reservedAt,
                        event.expiresAt
                    )
                )

                is ReservationCleared -> current.copy(
                    reservation = NoReservation
                )
            }
        }
    }
}

As you can see, the apply function does an exhaustive pattern match of the specified event and builds the new state of the book using the copy() function, which allows focusing on the book attributes that actually need to change.

In the next section, we will look into how to write business logic for our book entity.

It’s Time for Some Business Logic

In the previous section, we just saw how to build the state of our book entity using events exclusively. We did not implement any business or domain logic so far. Thus, it’s time to focus on implementing business logic.

I prefer to group each operation into its own package. That way, we organize our code in a way that reflects business capabilities. Ultimately, it comes down to a matter of personal preference.

Let’s start with an easy one: The feature of registering books is implemented in the RegisterBook object, which is contained in its own package. I use the specifier “object” to avoid the need of creating instances. The following code snippet shows the implementation of the register book functionality.

package com.cassisi.book.register

import com.cassisi.book.BookId
import com.cassisi.book.BookRegistered

object RegisterBook {

    fun handle(command: RegisterBookCommand): BookRegistered {
        return BookRegistered(command.bookId)
    }

}

data class RegisterBookCommand(val bookId: BookId)

The handle method just takes a RegisterBookCommand and returns an BookRegistered event with the book identifier contained in the command. Calling this function basically creates a book entity. Most scenarios, however, require the current state of an entity in order for the command to be executed against it, as we will see in the next example.

Let’s consider the functionality of borrowing books. As this is another feature, it is contained in its own package and file. The BorrowBook implementation does not only take a specific command but also the current state of a book entity. Furthermore, it requires a (pseudo) policy for validation purposes, which could also be considered a domain service from DDD.

package com.cassisi.book.borrow

import com.cassisi.book.*
import com.cassisi.book.BookReservedByOtherReader
import com.cassisi.reader.ReaderId
import java.time.LocalDate
import java.util.*

object BorrowBook {

    fun handle(command: BorrowBookCommand, current: Book, policy: BorrowBookPolicy): Result<BookBorrowed> {
        // validate if book is available
        if (current.state != BookState.AVAILABLE) {
            return Result.failure(BookAlreadyLoan(current.bookId))
        }

        // validate that book was not reserved by someone else
        if (current.reservation is ActiveReservation) {
            if (current.reservation.readerId != command.readerId) {
                return Result.failure(BookReservedByOtherReader(command.readerId, current.reservation.readerId))
            }
        }

        // validate if student borrow policy
        val result = policy.validateIfStudentIsAllowedToBorrowBook(command.readerId)
        result.onFailure { return Result.failure(it) }

        // the book can be borrowed, so we create the loan data
        val loanId = LoanId(UUID.randomUUID())
        val startDate = command.startDate
        val endDate = startDate.plusWeeks(6)

        // create event
        val bookBorrowedEvent = BookBorrowed(
            current.bookId,
            command.readerId,
            loanId,
            startDate,
            endDate
        )

        // return a result containing the event
        return Result.success(bookBorrowedEvent)
    }
}

data class BorrowBookCommand(
    val readerId: ReaderId,
    val startDate: LocalDate
)

The handle method implements the domain and business logic required for borrowing a book. It validates whether the command is allowed to be executed or not. Unlike the previous example, this handle method can actually fail, which is why I prefer to return a Result object rather than throwing exceptions. In the case of failures, a well-defined business exception is returned. Should the execution be successful from a domain perspective, a Result object containing the BookBorrowed event is returned.

That is it! We implemented an event-sourced domain model using functional code only. We will have a look into how the domain model can be tested in the next section.

Improved Testability

Functional programming promises better testability and predictability. In fact, the event-sourced domain model that we have built so far is highly testable. All code consists of functions and immutable data structures, with its data accessible from anywhere.

It is very straightforward to test the domain model, as shown in the following code snippet. All we have to do is to specify a command and the current state of a book, call the handle method of the feature we want to test, and then compare the result with what we expect it to be. This approach allows testing many different scenarios (meaning: different command and state combinations) and test whether they behave as expected. The following code snippet contains three test cases: one for registering a book and two for borrowing a book.

class BookTest {

    @Test
    fun registerBook() {
        val bookId = BookId(UUID.randomUUID())
        val command = RegisterBookCommand(bookId)
        val bookRegistered = RegisterBook.handle(command)

        Assertions.assertEquals(bookId, bookRegistered.bookId)
    }

    @Test
    fun borrowBook() {
        val bookId = BookId(UUID.randomUUID())
        val readerId = ReaderId(UUID.randomUUID())
        val startDate = LocalDate.now()
        val expectedEndDate = startDate.plusWeeks(6)

        val command = BorrowBookCommand(readerId, startDate)
        val current = Book(bookId, AvailableForLoan, NoReservation, BookState.AVAILABLE)
        val policy = BorrowBookPolicy()

        val result = BorrowBook.handle(command, current, policy)

        Assertions.assertTrue(result.isSuccess)

        val event = result.getOrThrow()

        Assertions.assertEquals(bookId, event.bookId)
        Assertions.assertEquals(readerId, event.readerId)
        Assertions.assertEquals(startDate, event.loanDate)
        Assertions.assertEquals(expectedEndDate, event.loanEndDate)
    }

    @Test
    fun borrowBook_alreadyLent() {
        val bookId = BookId(UUID.randomUUID())
        val readerId = ReaderId(UUID.randomUUID())
        val startDate = LocalDate.now()
        val expectedEndDate = startDate.plusWeeks(6)

        val activeLoan = ActiveLoan(
            LoanId(UUID.randomUUID()),
            readerId,
            startDate,
            expectedEndDate,
            0
        )

        val command = BorrowBookCommand(readerId, startDate)
        val current = Book(bookId, activeLoan, NoReservation, BookState.BORROWED)
        val policy = BorrowBookPolicy()

        val result = BorrowBook.handle(command, current, policy)
        Assertions.assertTrue(result.isFailure)

        val exception = result.exceptionOrNull()!!
        Assertions.assertEquals(BookAlreadyLoan::class, exception::class)
    }

}

Application Services and Infrastructure

Similar to my previous post, I group application-specific software in a different software module. For example, in order to borrow a book from an application perspective, the book entity must first be loaded from the event store, then validated against the according command, and finally, the new events must be stored in the event store. This flow is application-specific and should therefore be kept outside the domain.

Implementing application services is mostly straightforward. I prefer to group and implement each use case in its own package. Not only does this approach respect the Single Responsibility Principle (SIP) but also decreases coupling points.

The borrow book use case implementation is shown in the following code snippet.

class BorrowBookExecutor(
    private val repository: BorrowBookRepository,
    private val policy: BorrowBookPolicy
) : IBorrowBook {

    override fun execute(bookId: BookId, readerId: ReaderId, loanAt: LocalDate): Result<Unit> {
        val currentBook = repository.get(bookId)
        val command = BorrowBookCommand(readerId, loanAt)
        val result = BorrowBook.handle(command, currentBook, policy)
        return result.fold({
            repository.save(bookId, it)
            Result.success(Unit)
        }, { Result.failure(it) })
    }
    
}

If the execution of the handle() method was successful, the new event is stored in the event store. In case of failures, the execute method returns a failure containing the business exception.

Note that the implementation uses its own repository, which contains a contract with two methods: get() and save(). An in-memory event store is used for the purpose of this example. The implementation of that interface could then look as follows:

class BorrowBookEventStoreRepository : BorrowBookRepository {

    override fun get(bookId: BookId): Book {
        val events = SimpleEventStore.getEvents(bookId)
        return events.fold(Book.empty()) { acc, event -> Book.apply(event, acc) }
    }

    override fun save(bookId: BookId, changes: BookBorrowed) {
        SimpleEventStore.appendEvents(bookId, listOf(changes))
    }
}

Replaying the state of a book entity requires nothing more than a left fold over the previous events, which were loaded from the event store.

Final Thoughts

In this blog post, I presented how we can build an event-sourced domain model following a functional approach. Functional programming and event sourcing complement each other very well, as both embrace the ideas of immutability and functions.

A functional domain model results in highly testable and predictive code. Testing is pretty much straightforward, as shown in this post. Often times, it is easier to test a functional model rather than an object-oriented model. Not only do we avoid race conditions thanks to the immutability of our data structures, we also have full access to variables (as they are public by default) and don’t need to find a way how to access and test private fields.

Functional programming does not yet seem to be as widely used as object-oriented programming. Therefore, functional code might look like a bit odd for someone used to develop domain models following an object-oriented approach. Improved testability, however, might push some developers toward the functional approach.

Whether to use a functional model or a more classical approach, such as mutable aggregates, may come down to personal preference or company regulations. Both approaches are valid and are equally well-suited for implementing an event-sourced domain model. As you might have noticed already, this blog post implemented the same domain model as described in my previous post 😉

Additional Links

Photo by Antoine Dautry on Unsplash