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, ItemAddedToCart, 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.

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 both the data and the business logic for a specific bounded context. Common examples of aggregates include entities such as User, Account, ShoppingItem, or Metric. 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.

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.

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 a message Broker - if enabled. The broker's role is critical as it distributes these events across other instances and applications that subscribe to them. This event-driven architecture ensures that all relevant systems receive the updates, allowing them to adjust their projections and read models accordingly.

Query

Queries, on the other hand, 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.

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 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 Repositories and Aggregates
  • Providing interfaces to the underlying 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.