Domain-Driven Design | Domenic Cassisi / Personal Blog Sun, 25 Sep 2022 13:07:35 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.2 /wp-content/uploads/2022/09/cropped-favicon-32x32.jpg Domain-Driven Design | Domenic Cassisi / 32 32 How to Build an Event-Sourced Domain Model – A Practical Introduction /2022/09/25/how-to-build-an-event-sourced-domain-model-a-practical-introduction/ Sun, 25 Sep 2022 13:07:34 +0000 https://dcassisi.com/?p=235 Domain-Driven Design (DDD) is a software development approach first described by Eric Evans. In my previous post, I described that the idea of Event Sourcing, CQRS, and DDD complement each other very well. Feel free to check out my previous post for more information.

Today, I would like to focus on combining Event Sourcing with DDD. Additionally, I want to illustrate that the “domain model” approach fits well with the clean architecture style described by Robert C. Martin. Depending on the source of reference, the clean architecture style is also known as onion architecture or hexagonal architecture. Although those notions might differ in detail, all of them focus on the separation of concerns by organizing software code into multiple layers, with the domain layer taking the center place.

DDD does not require Event Sourcing. If we, however, decide to use Event Sourcing for our domain model, we refer to our domain model as an event-sourced domain model. This definition, which I first read about in Learning Domain-Driven Design (Vlad Khononov), emphasizes that the domain model is stored as a sequence of events rather than its current state.

The example scenario

Domain-Driven Design is most suited for complex domains. In our case, we use a (rather simple) library domain for lending and returning books. A reader can borrow a book, return a book, reserve a book, clear a reservation, etc.

In a real-life application, there are many subdomains and bounded contexts to consider, for example, lending, shipping, catalog, charging, reader management, and the like. Today, we just focus on the lending subdomain.

The lending subdomain knows about books and readers. In addition, a book is associated with its loans. Both book and reader represent aggregates, while the concept of a loan is part of a book. It is to be noted that there are several options to build such a domain model, with each approach coming with a different set of trade-offs. Ultimately, the most appropriate solution strongly depends on the concrete business environment.

Fundamentals

Alright! Let’s jump into some source code. The following code snippets are written using the Kotlin programming language, nevertheless, the source code is rather straightforward and should look similar to many other higher-level programming languages. The full source code is available here.

The first thing to define is the concept of an aggregate. An aggregate can be clearly identified by its identifier (ID). A possible contract of an aggregate might look as follows:

interface Aggregate<ID> {

    fun getId(): ID

}

An event-sourced aggregate needs to be able to load its state from the previous sequence of events and return any changes (events) that have not yet been saved. Therefore, the concept of an event-sourced aggregate requires at least two more contracts.

interface EventSourcedAggregate<ID, EventType> : Aggregate<ID> {

    /**
     * Builds the current state of that aggregate
     * from the history of previously stored events.
     */
    fun loadFromHistory(events: List<EventType>)

    /**
     * Returns a list of events that have occurred
     * after the aggregate got initialized.
     */
    fun getChanges(): List<EventType>

}

Note about versioning: Event Sourcing often uses optimistic locking for appending events to an event store. This requires applying some kind of versioning to our aggregates in order to check if the aggregate was updated simultaneously, e.g. by another thread or process. In this blog, however, we omit the versioning aspect for reasons of clarity. Refer to my more feature-rich library implementation for how versioning can be implemented.

A straightforward base class implementation of that interface might look like the following class:

abstract class BaseAggregate<ID, EventType> (private val id: ID): EventSourcedAggregate<ID, EventType> {

    /**
     * The list changes (events) stored as a mutable list.
     */
    private val changes = mutableListOf<EventType>()

    override fun getId(): ID {
        return this.id
    }

    override fun loadFromHistory(events: List<EventType>) {
        events.forEach { handleEvent(it) }
    }

    override fun getChanges(): List<EventType> {
        return this.changes.toList()
    }

    /**
     * Adds the specified event to the list
     * of changes and invokes the handleEvent()
     * method for applying that event.
     */
    fun registerEvent(event: EventType) {
        changes.add(event)
        handleEvent(event)
    }

    /**
     * This method is invoked whenever a new event is
     * registered. Implement logic here to change current
     * state of the aggregate.
     */
    protected abstract fun handleEvent(event: EventType)

}

This is everything required to implement a straightforward event-sourced domain model. Now, it is time to use that base class and implement a concrete aggregate.

The Book aggregate

I like to separate an aggregate’s contract and implementation, as it allows concentrating more on its behavior and what an aggregate should look like from the outside. The contract of the Book aggregate looks as follows:

sealed interface Book : EventSourcedAggregate<BookId, BookEvent> {

    fun borrowBook(readerId: ReaderId, startDate: LocalDate, policy: BorrowBookPolicy): Result<Book>

    fun returnBook(returnDate: LocalDate): Result<Book>

    fun reserveBook(readerId: ReaderId, reservationDate: LocalDate): Result<Book>

    fun clearReservation(): Result<Book>

}

Note that this source code is very explicit and uses domain-specific types rather than generic types, wherever appropriate. For example, instead of specifying a UUID as a reader id, we define a special type ReaderId that wraps the corresponding UUID. This approach respects the ubiquitous language principle from DDD.

We use Kotlin’s Result type to make behavior even more specific. Instead of throwing exceptions in case of validation errors, which are often invisible from the invoking site, we return a result object that indicates the outcome of an executed command. The caller can then handle both successful executions and error conditions.

The book aggregate implementation contains all domain logic for executing commands. This is where all the complex domain logic lives. The following code snippet shows the book aggregate class and the borrow method.

class BookAggregate(id: BookId) : Book, BaseAggregate<BookId, BookEvent>(id) {

    private var currentLoan: Loan = NoLoan
    private var currentReservation: Reservation = NoReservation

    override fun borrowBook(readerId: ReaderId, startDate: LocalDate, policy: BorrowBookPolicy): Result<Book> {
        // check if this book is already loan
        if (currentLoan is ActiveLoan) {
            return Result.failure(BookAlreadyLoan(getId()))
        }

        // check if there is a reservation that was made by another reader
        if (currentReservation is ActiveReservation) {
            val reservation = (currentReservation as ActiveReservation)
            if (reservation.readerId != readerId) {
                return Result.failure(BookReservedByOtherReader(readerId, reservation.readerId))
            }
        }

        // validate student borrow policy (max number of lent books reached etc. 
        val result = policy.validateIfStudentIsAllowedToBorrowBook(readerId)
        result.onFailure { return Result.failure(it) }


        // the book can be borrowed, thus an event is created
        val loanId = LoanId(UUID.randomUUID())
        val endDate = startDate.plusWeeks(6)
        val event = BookBorrowed(
            getId(),
            readerId,
            loanId,
            startDate,
            endDate
        )
        registerEvent(event)

        // clear reservation if book was reserved
        if (currentReservation is ActiveReservation) {
            clearReservation()
        }

        // return current instance
        return Result.success(this)
    }
    
    // ... other methods omitted for clarity reasons ...
     
}

First, we validate whether the command can be executed or not. In case of a validation error, we return a result object with a well-defined business exception indicating what went wrong.

After successful validation, a BookBorrowed event is created and added to the list of changes. The registerEvent(event) method in turn invokes the handleEvent() method, which applies changes to the current state of the aggregate. The following code snippet shows the handle method, which performs an exhaustive pattern match of the specified event:

    override fun handleEvent(event: BookEvent) {  // <<--- invoked by registerEvent()
        when (event) {
            is BookRegistered -> handle(event)
            is BookBorrowed -> handle(event)
            is BookReturned -> handle(event)
            is BookReserved -> handle(event)
            is ReservationCleared -> handle(event)
        }
    }

    private fun handle(event: BookBorrowed) {
        this.currentLoan = ActiveLoan(    // <<<---- apply changes to current state
            event.loanId,
            event.readerId,
            event.loanDate,
            event.loanEndDate,
            0
        )
    }

Note that we first register an event and then apply changes to the current state by handling that event. Only then, we can ensure that our application state is built exclusively from events. We could also write a separate state class for keeping the current state of an aggregate, which might be better suited for more complex aggregates with many child entities and value objects.

Testing the implementation is straightforward and does not require any infrastructure, as shown in the following code snippet:

class BookTest {

    @Test
    fun borrowBook() {
        val bookId = BookId(UUID.randomUUID())
        val book = BookFactory.registerNewBook(bookId)

        val readerId = ReaderId(UUID.randomUUID())
        val today = LocalDate.now()
        val endDate = today.plusWeeks(6)
        val policy = BorrowBookPolicy()
        book.borrowBook(readerId, today, policy)

        val event = book.getChanges().last()
        Assertions.assertTrue(event is BookBorrowed)

        val bookBorrowed = event as BookBorrowed
        Assertions.assertEquals(bookId, bookBorrowed.bookId)
        Assertions.assertEquals(readerId, bookBorrowed.readerId)
        Assertions.assertEquals(today, bookBorrowed.loanDate)
        Assertions.assertEquals(endDate, bookBorrowed.loanEndDate)
    }

}

For the full implementation of the book aggregate, see my GitHub repository.

The Use Cases Layer

The next layer we will look into is the use cases layer, which contains application-specific software. This layer controls the flow of aggregates, including storing and retrieving aggregates to and from the event store, respectively. I prefer to organize code in this layer by feature. Each feature or use case is contained within a corresponding package, as illustrated in the following figure.

The package structure in the use case layer

For each use case, there is a contract that can be invoked by the next outer layer. Organizing code this way respects the Single Responsibility Principle (SRP), so each class or interface has only one reason to change, which is a change of the according use case itself.

For example, the contract of the borrow book use case looks as follows:

sealed interface BorrowBook {

    fun execute(command: BorrowBookCommand): Result<Unit>

}

data class BorrowBookCommand(
    val bookId: BookId,
    val readerId: ReaderId,
    val loanAt: LocalDate
)

Similar to our domain model, we also return a result object indicating the outcome of the use case execution. There is no need for components which invoke that use case to know details on the implementation. The use case’s implementation is shown in the following code snippet:

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

    override fun execute(command: BorrowBookCommand): Result<Unit> {
        // load book aggregate from repository
        val book = repository.get(command.bookId)
        
        // try to borrow book
        val result = book.borrowBook(command.readerId, command.loanAt, policy)
        
        return result.fold(
            {
                // on success
                repository.save(it)
                Result.success(Unit)
            }, 
            {
                // on failure
                Result.failure(it)
            }
        )
    }

}

First, the according book aggregate is loaded from the repository, which is passed as a constructor argument. Second, the borrowBook method of that book is called passing the data of the specified command. When the execution of the command was successful, we store changes of that aggregate in the event store. In case of a validation error, we return a failed result object and pass the business exception. Alternatively, we could map the business exception to a more “generic” or application-specific exception containing an error code.

I prefer to build a separate repository interface for each use case in order to reduce coupling points. Furthermore, those repository interfaces are not very complex, as shown in the following code snippet. The implementation can still use generic base classes in the infrastructure layer to avoid redundant code if desired.

interface BorrowBookRepository {

    fun get(bookId: BookId): Book

    fun save(book: Book)

}

Infrastructure and additional notes

Since the focus of this blog is on the domain and application layer, we won’t go into detail about the infrastructure layer. All infrastructure concerns are outsourced to the infrastructure layer. This is the layer where we would implement the logic for storing events in an event store such as EventStoreDB. This is also the layer where we would invoke the use cases layer from different entry points, for example, an HTTP endpoint, a GUI, or a console, and present results to the user.

Where do you do null checks and the like?

As the use cases layer as well as the domain layer uses domain-specific types that already ensure “valid” data, validations like null checks, empty strings, and the like are up to the infrastructure layer. The infrastructure layer converts data from the form most convenient for the infrastructure layer (e.g. a JSON data transfer object) to the form most convenient for the use cases (and domain) layer. In my opinion, it makes sense to avoid polluting the domain with those “annoying” validations and allow focusing on core domain logic instead.

Is the presented approach appropriate for every use case?

Obviously not. Combining Event Sourcing, DDD, and Clean Architecture is neither necessary nor reasonable for all use cases. Simpler subdomains with less complex logic might profit from a more straightforward approach, such as the Transaction Script pattern.

Outlook

Thanks for reading this post. I hope it gave you some insights on how to build an event-sourced domain model.

Although very popular, the tactical patterns from DDD (aggregates, value objects, entities, …) are just one way to build an event-sourced domain model. In the next post, I would like to present an alternative approach that I find worth considering when building an event-sourced domain model. Stay tuned and see you in the next one.

Photo by Hasan Almasi on Unsplash

]]>
Event Sourcing, CQRS, DDD, and event-driven microservices. What did I learn from my master’s thesis? /2022/09/18/event-sourcing-cqrs-ddd-and-event-driven-microservices-what-did-i-learn-from-my-masters-thesis/ Sun, 18 Sep 2022 19:04:02 +0000 https://dcassisi.com/?p=167 From the beginning of March to the end of August 2022, I wrote my master’s thesis in the field of software architecture. The complete title of my thesis is as follows:

Event Sourcing, CQRS, and Domain-Driven Design, and Their Application to Event-Driven Microservices

The title on its own involves quite a number of concepts already. There is no chance to cover all of them in extensive detail in this blog post. Rather, I would like to share some of my findings and things I learned during writing my thesis. For this reason, the present post requires a basic understanding of the concepts discussed. Some sections refer to a proof of concept, which is an event-driven library application, that I developed as part of my thesis.

Does combining event sourcing, CQRS, and DDD make sense?

Foremost, I find it astonishing how well event sourcing, CQRS, and domain-driven design (DDD) complement each other. Although all concepts are fundamentally different and have totally different purposes, they work very well in conjunction. But why is that? Well, I came up with several reasons that I want to describe briefly in the following:

First, CQRS is a natural consequence of event sourcing. According to Greg Young (who coined the notion of event sourcing and CQRS), CQRS was always intended to be a stepping stone toward the idea of event sourcing. Although CQRS can be applied without event sourcing, it actually originates from the event sourcing concept.

Second, all concepts focus on behavior and business capabilities rather than on storing state or other technical aspects. DDD focuses on the domain, its natural boundaries, language, and behavior. Everything within that domain is made explicit, for example by defining bounded contexts and domain events. We can store those domain events in a proper event store and build the complete application state from our events. As event sourcing is not (always) appropriate for queries, we can apply the CQRS principle and implement read models better suited for the execution of queries.

Third, all concepts are pretty straightforward to apply, however, they come with a steep learning curve. It took me a while to actually understand these concepts and answer very detailed questions, but I think it is worth it. Event sourcing and CQRS are gaining popularity. DDD seems to be well established in many industries and software companies.

To provide a short answer to the question above: Yes, the combination of event sourcing, CQRS, and DDD makes sense when we want to make things explicit in a domain or some part of it.

How do these concepts apply to event-driven microservices?

Microservices are rooted in the ideas of domain-driven design, especially in the notion of a bounded context. In my research, I found that every microservice is a bounded context, but not every bounded context is a microservice. Aligning microservices with subdomains seems to be a safe heuristic. Subdomains can be identified as part of the strategic design principles of DDD. I could successfully apply this approach in my proof of concept, where I was able to do a one-to-one mapping of subdomain and (event-driven) microservice.

Not every microservice is an event-driven microservice. An event-driven microservice shares many characteristics of the traditional microservices architecture style, though. Event-driven microservices should be autonomous and asynchronously communicate with other services through events.

The inter-service communication aspect is exactly where event sourcing can be handy. Since events are already modeled as domain events within a service boundary, it is more straightforward to use those domain events to communicate with other services. However, usually, we don’t want to use internal domain events to communicate with other services, because domain events leak data that might not be understood by other services. Furthermore, it might hurt the services’ autonomy. Instead, we “transform” those events into some kind of thin events. By the way, EventStorming can help with identifying those special events, which in this context are referred to as Pivotal Events.

I had the privilege to talk with Mauro Servienti, a highly experienced solution architect, and he stated that the highest degree of autonomy can be achieved by following a share-nothing policy. Sharing data should be limited to very stable data, such as identifiers. In my proof of concept, all events that are used to communicate between service boundaries consist of identifiers only. As identifiers are not expected to change, we can consider them stable. A thin event that notifies other service boundaries about a student that was matriculated could look as follows:

student matriculated event:
{

  "studentId": "01e2c0c8-d4f5-43af-a9b4-6d8c12933203"
  
}

Are these concepts in combination a silver bullet?

As with everything in software architecture, all concepts come with benefits and shortcomings. This also applies to the combination of event sourcing, CQRS, DDD, and event-driven microservices. Especially when working on my proof of concept, I realized that the concepts’ combination might not always be beneficial. Even though they come with a great amount of flexibility and great scalability options, applying event sourcing and CQRS to event-driven microservices increases the system’s complexity a lot. Remember that for every event-sourced microservice, you need to maintain one event store instance, at least one projection, and at least one read model. Each contributes to the overall complexity of the system and represents moving pieces within the architecture. Although these building blocks are usually not very complex, their impact must still be taken into consideration when designing a solution architecture, in my opinion. In the case of my proof of concept, at least one of my four service boundaries could be implemented without event sourcing and CQRS, as it mainly represented traditional CRUD behavior.

In conclusion, I found that the strategic design principles of DDD can be applied to the complete solution architecture, but we want to be a bit more careful when it comes to event sourcing and CQRS. Event sourcing might not be appropriate when there is not much behavior in the domain/subdomain. CQRS might not be necessary if there is just one view of the data within a domain, or if scaling the command and query side independently is not essential.

My takeaways

The following list shows some of the results and personal findings that I find worth mentioning when it comes to what I have learned from my master’s thesis:

  • Focus on business capabilities, not entity services.
  • Don’t think of microservices, but rather in service boundaries.
  • There are problems that are not technical, even if they look alike.
  • Concentrate on what’s really important from a business perspective.
  • Dive deep into how the domain works, which helps to handle special cases and race conditions.

This short post just covered some aspects of my thesis, in fact, it touched on many more interesting areas, which cannot be part of this very first blog post. Some of the concepts discussed in this post will probably be covered in future posts. I am looking forward to writing about more specific aspects and approaches in future blog posts, and highly appreciate it if you check them out as well.

Thanks!

]]>