Domenic Cassisi Personal Blog Thu, 04 Apr 2024 21:53:43 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.2 /wp-content/uploads/2022/09/cropped-favicon-32x32.jpg Domenic Cassisi 32 32 Certified Professional for Software Architecture – Foundation Level | My Preparation and Exam Experience /2024/04/04/certified-professional-for-software-architecture-foundation-level-my-preparation-and-exam-experience/ Thu, 04 Apr 2024 21:53:41 +0000 /?p=358 As an aspiring software and solutions architect, I decided to get certified in the field of software architecture. However, I quickly noticed that there are not many certifications out there. Compared to other IT skills, such as cloud or programming languages, the number of certifications in the field of software architecture is quite limited. In fact, the only certification I found that truly focuses on software architecture principles is the Certified Professional for Software Architecture (CPSA) provided by the International Software Architecture Qualification Board (iSAQB).

The iSAQB provides two certifications for IT professionals: The CPSA-F (Foundation Level) and the CPSA-A (Advanced Level). To get CPSA-F certified, it is sufficient to pass a multiple choice and multiple select certification exam. The advanced level certification requires one to not only pass the CPSA-F exam, but also attend additional training, submit a project, and defend your solution against an audience.

To start my certification journey, I decided to sit the CPSA-F (Foundation Level) exam. I won’t talk about the advanced-level certification. If you want to learn more about it, check out the iSAQB website.

Exam Preparation – The Common Way

Again, to get certified, you have to pass a multiple choice exam, which consists of a bit more than 40 questions and comes with three different question formats (more on that later).

But how does one usually prepare for this exam? It is generally recommended to attend one of the official training sessions that you can find on the iSAQB website. The trainers are accredited professionals that can best prepare you for the exam. The training typically lasts three to four days, upon which you typically take the CPSA-F exam.

The combined costs for training and exam fees will be around €2500 – €3000, though the training costs will make out the biggest cost factor, while exam fees are “just” around €300 (sometimes a bit less). If you are working for a medium- or large-sized company, there might be a chance that this training will be paid for you, although this is not a guarantee.

After the training and some review of the study materials, you should be ready to sit and pass the exam.

But what happens if you (or your company) cannot or do not want to afford a €2500 training, but you still want to get certified? Good news: You can prepare yourself for the exam. Let’s look into it.

Exam Preparation – Self-Study

It is absolutely possible to self-study for the CPSA-F level exam and pass it. I did it myself. Preparing myself for the exam was a rewarding experience. Additionally, it helped me to save a lot of money. Let’s see how I did it.

My main preparation consisted of the following two study resources (books):

  • “Software Architecture Foundation – 2nd edition” (in English)
  • “Basiswissen für Softwarearchitekten: Aus- und Weiterbildung nach iSAQB-Standard zum Certified Professional for Software Architecture – Foundation Level (5. Auflage)” (in German)

The first book is a concise study guide with explanations and descriptions of what you need to know and focus on. It’s written in clear and easy-to-understand language and even comes with some questions to test your knowledge of the various learning goals.

Yes, the 2nd resource is in German. But there is more literature available in English; feel free to check out the exam page for more references. Anyway, I also chose it because the book just came out when I started to prepare for the exam – simply good timing.

I read both books two times and even wrote a summary to manifest my learning. All of that while I was still working full-time. Which is why it took me a bit longer. I studied for 10 to 20 minutes every day, for two to three months. You can probably do it much faster than that, however, for me, it was important to not only prepare for the exam but also to extract as much knowledge as possible. Furthermore, why should I stress myself? This is the beauty of self-paced learning.

Anyway, besides these two books, I highly recommend checking out the official mock exam with practice questions, glossary, and exam curriculum. You can find all of that on the iSAQB website.

The next step lies ahead: Sitting the exam.

The Exam

As I was feeling prepared enough, I registered for the exam. I decided to take the exam offline – in a test center. I prefer test centers over online exams as I am always afraid of outages and loss of my Internet connection. But I guess this is more of a personal preference, so choose whatever best suits your situation.

Let’s quickly talk about the three different types of questions that await you in the exam:

  • A-question: These are multiple-choice questions. Out of a given set of answers, only one of them is correct.
  • P-question: These are multiple selection questions. You are given the number of correct options to select.
  • K-question: These questions ask you to categorize (all) answers (e.g. true/false, advantage/disadvantage, …). This type of question was new to me and might be the most difficult one to get (completely!) right.

You have the chance to get fractional points for partially answered P- and K-questions. This is not the case for A-questions. So let’s say you have a P-question and you are pretty sure about one answer option out of four, but not about the others. If you are right (and you left the other options neutral), you will still get 0.5 points.

I don’t remember the exact number of questions in my exam, but it was around 42 questions, covering pretty much all learning goals in the curriculum. I had the impression that the actual exam questions were more difficult than the exam questions provided in the official mock exam. Furthermore, I found the exam does not rely too much on just asking for facts (which is nice); being able to apply your knowledge is at least as important as understanding specific terms.

Some questions were just weird. Even after reading them multiple times, I wasn’t sure if I got what was asked for. In such cases, marking these questions for review and coming back to them later worked well for me. In any case, you don’t need 100% to pass the exam, 60%+ is enough.

The time you got for the exam is 75 minutes, I used every minute of it. When I answered the last question, I still had about 20 minutes left to review my answers, which I took advantage of. But don’t stress yourself too much on a single question. If you are not sure, listen to your inner self, choose the answers, and move on.

Exam Results

I passed the exam! At this point, I was just happy. I scored around 87%. I think this shows that self-study is a viable option if attending dedicated training is not possible in your situation.

As a result of my self-study and exam experience, I crafted some practice and exam questions that I wish I had when preparing for the exam. If you want to learn more and support me, please check out my course on Udemy.

https://www.udemy.com/course/cpsa-f-practice-exams/?referralCode=C53EAD9212910F467AEA

I hope this post helped you learn more about the CPSA-F certification, how to prepare for it, and what the actual exam might look like.

Good luck on your certification journey!

]]>
Why is Kafka not Ideal for Event Sourcing? /2023/05/06/why-is-kafka-not-ideal-for-event-sourcing/ Fri, 05 May 2023 22:23:17 +0000 /?p=316 Kafka is a powerful streaming and messaging platform that is often used to build large-scale systems. There are many use cases for which Kafka is very well suited, but from time to time Kafka gets confused with concepts that it was originally not designed for. One of these concepts is event sourcing.

The Apache Software Foundation describes Kafka on its website as follows:

Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.

(Source: https://kafka.apache.org/)

Personally, I like vendor descriptions like this because they aim to emphasize exactly what kind of problems a specific tool solves. However, in this case, there is not a single word about event sourcing. When having a closer look at the documentation, there is indeed a small paragraph about event sourcing, but it lacks details.

It happens frequently that I stumble across posts, videos, and sometimes even books that mention Kafka can be used for event sourcing. While that is not completely wrong, it still raises whether a streaming platform like Kafka is actually able to perform the tasks of an event store. This is exactly what I would like to discuss in this post.

What is an Event Store?

An event store is a type of database whose primary focus is on storing and receiving events. In contrast to other database types such as relational databases, event stores “natively” support the append-only semantics and immutability principles of events.

Remember, events cannot be modified once there are created because they represent facts that have already happened within a domain. This is the reason why events are immutable.

The Relation between Event Sourcing and Event Stores

Event Sourcing, in its most basic form, is nothing more than a persistence mechanism. Instead of writing the current state of an object to, let’s say, a relational database, we store a series of events associated with that object in a specific database: the Event Store.

In summary, event sourcing is the persistence mechanism, while the event store is the actual database where the events for each object are persisted.

Note: While there are databases built from the ground up for event sourcing, I also saw event stores that are built on top of established databases, such as PostgreSQL.

If you want to learn more about event sourcing, feel free to check out my other blog posts or refer to blog articles from EventStoreDB.

Kafka + Event Sourcing?

Kafka is a great and powerful tool when it comes to streaming systems, but is it also ideal for event sourcing? The next subsections aim to give an idea of whether Kafka can actually fulfill the typical requirements of an event store.

Optimistic Concurrency Control

A common requirement of a database made for event sourcing is to detect concurrent writes. This is typically achieved by using some kind of optimistic concurrency control. Optimistic concurrency control ensures that decisions are made based on the current state of an object, which is a guarantee that many databases provide. One way to implement this is by using a version number that the event store can use to detect whether an event stream was modified between the read and write action. This approach is successfully used in EventStoreDB, for example.

Kafka does not support optimistic concurrency control. However, another blog article mentioned that it is possible to lock a whole partition, which would at least allow for pessimistic concurrency. Either way, optimistic locking is a much more scalable and performant solution compared to pessimistic locking. Many other viable solutions, such as a “database frontend”, can mitigate this issue but often result in additional complexity.

Efficient Loading of Single Entities

A very common use case of an event store is to load an entity by its identifier. An event store persists an entity’s events in an event stream. When we want to load and reconstruct an entity’s state in memory, we have to load the events contained in the corresponding event stream. Event stores such as EventStoreDB are optimized for this use case.

In contrast, Kafka deals with topics. A topic typically represents a whole type of entity, for example, customers, orders, or sessions. Unfortunately, there is no practical way to load a specific entity by its identifier within a topic. We can assume to deal with linear complexity here.

What feels more natural to you: Loading a single event stream by its ID, or (potentially) loading a whole topic and iterating over that topic to filter out events for a given entity?

Consistent Writes

If a series of events is appended to an event stream, we want to make sure that all our events are persisted in one atomic transaction. Partially written or duplicated events would lead to consistency issues. This kind of transaction support is a matter of course for event sourcing solutions like EventStoreDB. In the case of EventStoreDB, this concept is better known as atomic writes.

Actually, Kafka comes with support for transactions for a few years now. Even though Kafka supports transactions, more work is required to accomplish the same effect compared to using an event store solution that provides this guarantee out of the box.

Storing and Publishing the Same Events?

While using events as a persistence mechanism, publishing events to notify other components about a change are two different scenarios. The first scenario typically deals with domain events, while the latter is subject to integration events. Still, there are a lot of cases where the same events are used for persistence and integration.

Events that are stored as part of the persistence mechanism (event sourcing) have nothing to do with the events that are used to notify other components about a state change in the system. Note that these potential components could be other service boundaries.

Event sourcing is just an implementation detail. The question to ask is whether we really want to expose those implementation details outside our service boundary. It feels a bit like pushing a service’s data store into other services’ faces and saying: “Here! Take all my data and do whatever you want with it.”

From my experience, we don’t want other components or services to read data directly from our database. Instead, we want to control which data to expose and which to keep inside our own boundary through a well-defined contract, e.g., an API.

Use the Right Tool for the Job

If you decide to use event sourcing for parts of your system, you probably have a good reason to do so. In such cases, the use of proper tooling is recommended to avoid additional complexity. The event sourcing approach is just different and sometimes even confusing if not understood completely compared to more traditional approaches. Using tools that were just not made for event sourcing won’t make our lives easier.

Again, Kafka is a great streaming and messaging platform, and it is designed to solve a specific set of problems. This set of problems does not necessarily include how event-sourced entities should be stored. On the other hand, an event store is designed for a different set of problems, and certainly cannot (or should not) be used for use cases for which Kafka was designed.

In my opinion, using the right tool for the job should be the preferred choice, given that all organizational as well as technical constraints allow the use of these tools.

Conclusion

In conclusion, can Kafka be used for event sourcing? Yes.
Is Kafka an ideal solution for event sourcing? No.

Kafka is a powerful tool but not the most ideal solution for event sourcing because event stores come with requirements that are just more complex and challenging to meet for a streaming platform like Kafka.

In the end, one has to make the tradeoff analysis and ultimate decision on whether to use an established event sourcing solution or Kafka. What remains is the following: A great messaging platform cannot also be the best event store and vice versa.

Further reading

If you want to read more about this topic and dedicated event store implementations, check out the following links:

  • https://www.eventstore.com/
  • https://martendb.io/
  • https://event-driven.io/en/event_streaming_is_not_event_sourcing/
]]>
Functional Programming + Event Sourcing = A Good Idea? /2022/10/13/functional-programming-event-sourcing-a-good-idea/ Thu, 13 Oct 2022 21:18:22 +0000 https://dcassisi.com/?p=261 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

]]>
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!

]]>