Software Architecture | Domenic Cassisi / Personal Blog Wed, 16 Jul 2025 07:28:15 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.2 /wp-content/uploads/2022/09/cropped-favicon-32x32.jpg Software Architecture | Domenic Cassisi / 32 32 Commands, Queries, and Events /2025/07/15/commands-queries-and-events/ Tue, 15 Jul 2025 21:46:15 +0000 /?p=370 This blog post is the first in a series I plan to write about software architecture, event sourcing, and related topics. The idea is to focus on fundamental concepts and core principles, while staying as tech-agnostic as possible.

It’s easy to get lost in technical jargon or framework-specific details, which often have zero impact on the concepts themselves.

Today, I want to talk about three very important concepts:

  • Commands
  • Queries
  • Events

You’ve probably heard these terms before — but do we really understand what they mean?

Any system contains at least one of the three, more often than not, all three, though. Let’s break them down one by one.

Commands

A command represents the intention to change the system’s state. Commands invoke behavior — they request the system to do something. They may result in side effects or business rule enforcement.

Examples:

  • Placing an order
  • Canceling a booking
  • Reserving a seat at the cinema
  • Logging out

Please note that a command is like a request for a state change; that’s why we use the term “intention” to describe it. The command does not guarantee that the system will change. A command can still fail during processing, for example, if business validation or other constraints are violated.

Commands have a clear ownership. The consumer of the command (often called “command handler”) logically owns the command and knows how to process it.

There may be many clients sending the same type of command to the system (e.g., via an HTTP POST API), but all of them will be handled by the same logical command handler. In essence, there can be many command producers, but there is only one logical command consumer/handler per command type.

The common naming strategy for commands is “verb + noun” in imperative form, for example “Place Order”, “Cancel Booking”, etc. This rule applies to both code and models, which makes it easy to find out how a particular command is implemented and handled in code.

Speaking of which, this is how a simple command could look like in (Kotlin) code:

Kotlin
data class PlaceOrder(
  val orderId: OrderId,
  val customerId: CustomerId,
  val productId: ProductId,
  val quantity: Int
)

Queries

A query is a request to read the state from the system. Most commonly, queries are used to read the current state from the system; however, it can be any state, really. A query could request anything, including:

  • Getting order details by order number
  • Listing all available rooms in a hotel
  • Finding all active users in the last 30 minutes

Unlike commands, queries don’t change the system’s state. They can be seen as side-effect-free operations. A query simply returns requested data. A query will logically (!) return the same results if no commands have been executed in the meantime.

Queries can be issued by many clients, just like commands. There is only one logical consumer per query type (sometimes called “query handler”), which also owns the query.

Queries follow a similar naming strategy to commands (verb + noun in imperative style), but they use verbs like “find”, “get”, “list”, and similar. For example: “Find Order By Id”, “List Available Rooms”, …

In code, a query could be represented as follows:

Kotlin
data class FindOrderById(
  val orderId: OrderId
)

Events

An event is a fact about something that has already happened in the system. A fact cannot be undone. Therefore, events are immutable. Events declare facts about important and relevant business decisions within the system.

The term “event” is very overloaded in our industry. Here we are not talking about events like “button clicked” or “mouse moved”. What we mean is raising a fact about a meaningful business decision that was made and that impacts the state of the system. With each event, the system moves forward. Time becomes important when we deal with events, as we are often interested in what happened when and in what order.

Events can be anything relevant in our context, for example:

  • Order Placed
  • User Password Changed
  • Booking Canceled
  • Session Closed

The publisher of the event is the logical owner of that event. Unlike commands and queries, there is only one logical producer of an event (type). However, there can be many (including zero) consumers of that event. Whoever is interested in that event could consume that event and react to it, e.g., to build a projection or to trigger another part of the workflow.

Note that we name events in the past tense (emphasizing that this is a fact that cannot be undone, as it already happened). In code, an event could look as follows:

Kotlin
data class OrderPlaced(
  val orderId: OrderId,
  val customerId: CustomerId,
  val productId: ProductId,
  val quantity: Int,
  val placedAt: Instant
)

Side-by-Side Comparison Table

The following table summarizes and compares the characteristics of commands, events, and queries.

AspectCommandQueryEvent
PurposeRequest to change stateRequest to read stateDeclare a fact about what happened
OwnershipConsumer of the commandConsumer of the queryPublisher of the event
ProducerZero or many (0..*)Zero or many (0..*)Exactly 1 per event
ConsumerExactly 1 (handler)Exactly 1 (handler)Zero or many (0..*)
NamingImperative (verb + noun)Imperative (with get, find, …)Past tense
ExamplePlace OrderFind Order By IdOrder Placed
Comparing commands, queries, and events

Conclusion

So to recap:

  • Commands ask the system to do something.
  • Queries ask the system for data.
  • Events tell us what has already happened.

I hope you enjoyed this comparison of these concepts.
If you were already familiar with them, I hope it served as a good refresher and helps you stay even more mindful during your next design session.

]]>
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 questions that I wish I had when preparing for the exam. These cover important areas of software architecture, which are also mentioned in the CPSA-F curriculum. If you want to learn more and support me, please check out my course on Udemy:

https://www.udemy.com/course/software-architecture-practice-questions/?referralCode=C53EAD9212910F467AEA

(Please note that I am not an accredited iSAQB trainer and I never claimed to be one.)

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!

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

]]>