Skip to content

Concept

comby is designed with Event Sourcing and Command Query Responsibility Segregation (CQRS) principles.

Event Sourcing

Event Sourcing is a technical approach used in software development to capture and persist the state of an application as a series of Events. Instead of directly storing the current state of an object, Event Sourcing focuses on recording every state change as an immutable Event.

In this pattern, Events represent significant actions or changes that occur within an object - named Aggregate or aggregate root. Each Event carries all the relevant information necessary to reconstruct the aggregate’s state at any point in time. These events are stored in an EventStore, which acts as the single source of truth for all aggregates for the whole application’s history.

Event

An Event represents a meaningful change in the state of an Aggregate that has occurred in the past. In an event-sourced system, rather than storing the current state of an aggregate directly, the system records every state-changing Event that has been applied to the aggregate over time. These events are immutable and stored in the EventStore as the source of truth, allowing the system to reconstruct the current or past states of an aggregate by replaying its events in chronological order.

Each event captures a specific action or occurrence in the domain, often described in business terms (e.g., UserRegistered, AccountDebited, OrderPaidEvent, MetricUpdated). Events are typically designed to be as descriptive as possible, including relevant data about the change such as timestamps, affected entities, and any contextual information needed to interpret the event.

Here is a very simplified representation of how events are stored in the EventStore:

AggregateDomainDomainEvt NameDomainEvt DataVersion
2c8fe....OrderOrderPaidEvent(Serialized JSON)2
8679a....PaymentPaymentReceivedEvent(Serialized JSON)6
2c8fe....OrderOrderPlacedEvent(Serialized JSON)1

The integral part of an Event is the actual content - the so-called Domain Event or in short form: DomainEvt. An event basically consists of the following information:

  • Aggregate - the unique identifier (uuid-v4) of the aggregate that the event belongs to
  • Domain - the domain name of the event like Account, Order, Payment, Tenant, etc
  • DomainEvt Name - the name of the domain event like OrderPaidEvent, PaymentReceivedEvent, OrderPlacedEvent, etc
  • DomainEvt Data - the actual data of the domain event, which is a serialized JSON object
  • Version - the version of the aggregate

Both information: Domain and DomainEvt Name combined into format Domain.DomainEvtName are key information in comby and must be unique in the system. The Version is used to ensure that events are applied in the correct order and to detect any inconsistencies. For example: If we want to load the above Aggregate with the identifier 2c8fe...., the associated events are applied to the (new) aggregate using the Version information. First the event with Version 1, then the (last) with version 2.

From the developer's perspective, events are fundamental to maintaining a consistent and accurate audit trail of all changes within the system. They also serve as triggers for reacting to changes, whether through updating read models in CQRS, notifying external systems, or generating additional events in response. Since events are immutable, they allow for powerful capabilities such as time travel, debugging, and retrospection of system behavior.

Aggregate

To reconstruct the current state of a single Aggregate, all Events in the EventStore belonging to this particular object are sequentially applied in order. By replaying the events, the aggregate’s state can be determined at any specific moment in time.

From the developer's perspective, an aggregate represents a core domain entity that encapsulates the business logic for a specific bounded context. Common examples of aggregates include entities such as User, Account, Order, or Payment. Each aggregate is responsible for maintaining its own consistency and enforcing business rules across its internal state, ensuring that any operations affecting it adhere to domain invariants.

Aggregates serve as the primary interface through which developers interact with the domain model, encapsulating related entities and value objects while handling complex workflows and ensuring that changes are persisted in the EventStore.

It is also important to note that aggregates are short-lived objects. When the system loads an aggregate, a new Aggregate instance is first created and then all associated events are applied. Only from this point on is an aggregate fully loaded and can process (new) business logic.

Command Query Responsibility Segregation (CQRS)

Command Query Responsibility Segregation (CQRS) is a software architecture pattern that separates the read and write operations of an application’s data model. It advocates using distinct models for handling read (Query) and write (Command) operations to achieve better scalability, performance, and maintainability. By decoupling the data models, CQRS allows optimizing each model based on its specific requirements, leading to a more efficient and flexible system design.

Both concepts were implemented in this framework and connected in the Facade. The facade is the central interface for utilizing the framework and is responsible for dispatching commands and queries, as well as handling events and distributing events through message brokers to other services.

Command

Commands are used to change the application's state, such as creating a new user or updating an existing record. When a command is issued, it is handled asynchronously by a CommandHandler within the Facade, which in turn generates new Events. Since commands are meant to change the application's state, they are not executed right away. Instead, a command is placed in a queue (the CommandBus) and processed asynchronously by the Facade later.

This means that when a command is issued, the user does not immediately receive the final result of the execution. Instead, only a RequestUuid is returned. This unique identifier allows the user to track the status of the original command. But living in the 21st century the Facade already provides a method WaitForCmd to allow the user to wait for a command to be completed.

Once the Facade processes the Command through the appropriate CommandHandler, it generates Events based on the Aggregate's intentions (or methods). These events are then published to the EventBus and - if enabled - to a message Broker.

Broker

The broker's role is crucial as it distributes new events across application instances. This event-driven architecture ensures that all relevant systems receive the updates, allowing them to adjust their projections and read models accordingly.

A broker is used when the application is scaled to multiple instances, but also to supply external applications with the events. Like all important classes, a Broker is simply an interface that needs to be implemented. comby currently already offers NATS.

Query

Queries, compared to Commands, do not modify the application's state. Instead, they retrieve data and are handled synchronously within the same requesting context. Queries are processed directly by QueryHandlers, resulting in a response returned to the user.

Commands and Queries are usually used in a REST API. They also have a Request Context or in short form: ReqCtx, which contains information about the initiator and the destination resource.

Facade

comby is designed to be extensible and configurable. It allows developers to customize and extend the framework to meet their specific requirements, such as adding new domains, integrating external services, or implementing custom event handlers.

The Facade is the central interface for utilizing the framework and is responsible for dispatching Commands and Queries, as well as handling Events and distributing events through a message Broker to other services. The facade is the entry point for interacting with the framework and provides a unified interface for executing commands, queries, and event handling.

Overall, the Facade offers the following components:

  • Registering Event, Command and Query Handlers
  • Providing interfaces for dispatching Commands and Queries
  • Handling Events and distributing them through a message Broker
  • Providing access to the Repositories
  • Providing access to the stores (EventStore, CommandStore, CacheStore and DataStore)
  • Proving many other helper functions for working with the framework

And most important:

  • Providing defaults implementation - which handles Tenants, Accounts, Groups, Permissions, and many other features out of the box.