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
:
Aggregate | Domain | DomainEvt Name | DomainEvt Data | Version |
---|---|---|---|---|
2c8fe.... | Order | OrderPaidEvent | (Serialized JSON) | 2 |
8679a.... | Payment | PaymentReceivedEvent | (Serialized JSON) | 6 |
2c8fe.... | Order | OrderPlacedEvent | (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 toDomain
- the domain name of the event likeAccount
,Order
,Payment
,Tenant
, etcDomainEvt Name
- the name of the domain event likeOrderPaidEvent
,PaymentReceivedEvent
,OrderPlacedEvent
, etcDomainEvt Data
- the actual data of the domain event, which is a serialized JSON objectVersion
- 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.