Enhancing Flexibility in Event Sourcing by Evolving Aggregate Types
I am Tomohisa Takaoka, CTO of the J-Tech Japan, We are currently developing Sekiban, an Event Sourcing/CQRS framework, at the company. It is an open-source framework, so we would be very happy if you could at least give it a star!
This article explores the benefits of changing aggregate types in Event Sourcing and DDD to enforce business rules and maintain consistency. It explains how aggregates manage state transitions, using type-based constraints rather than flags, to prevent errors and ensure correctness.
What is an Aggregate?
In Event Sourcing and Domain-Driven Design (DDD), an Aggregate is a concept that provides a consistency boundary to maintain the business rules of the domain. An aggregate groups together multiple entities and value objects and treats them as a single unit.
Key points of Aggregates
1. Consistency Boundary
An aggregate defines an important scope for maintaining business rules and consistency. When performing specific operations, changes within the aggregate are designed to maintain consistency within that boundary. In other words, since the data within the aggregate must always be consistent, it does not affect entities outside the aggregate.
2. Root Entity (Aggregate Root)
Root Entity The entity that serves as the center of aggregation, and access and operations to the entire aggregate are performed through this root entity. This prevents direct manipulation of entities inside the aggregate and maintains the consistency of the aggregate.
3. Encapsulation of Internal Details Entities and value objects inside the aggregate can only be manipulated through the root entity. This encapsulates the internal structure from the outside and ensures that the outside does not depend on the internal structure.
4. Unit of Change In event sourcing, an event occurs when an aggregate is changed. This event records the important business changes made by the aggregate, andThe change history is managed by aggregation unit.
Example
For instance, in the "Order" domain, the order itself becomes the aggregate, and it includes "order items (entities)" and "total amount (value objects)". Changes related to the order are made only through the root entity (order), and it is not possible to directly access and modify the internal entities or value objects. This ensures that processing is performed while maintaining the consistency of the entire order.
Significance of Aggregates
Consistency of business rules: In complex domain logic, ensure that business rules are always followed.
Clarify the scope of transactions: When consistency needs to be maintained beyond the scope of an aggregate, a separate aggregate is required.
Flexibility: Even as the domain becomes complex, the design can be kept simple by organizing it in aggregate units.
In this way, aggregates play an important role in DDD and event sourcing, serving as one of the pillars for making the domain model robust and consistent.
What does it mean for an aggregate's type to change?
In domain-driven design (DDD) and event sourcing, the concept of an aggregate's type changing refers to how the aggregate's internal state, behavior, and relationships with other aggregates can evolve throughout its lifecycle, reflecting the changes and progressions that occur in the domain.
It means that the behavior and allowed operations change. This allows imposing constraints through types to ensure that specific business rules and operations are followed, preventing mistakes.
For example, let's consider a shopping cart. Normally, when adding items, the cart must be in the "item selection" state. However, if items can be added after the order is finalized or payment is completed, problems may occur. For instance, there is a risk of accidentally shipping items that haven't been paid for, so it's necessary to control the cart's behavior based on its state.
Flag Management vs Type-based Constraints
Using Flags
Adding flags within an aggregate to track the current state (e.g., order confirmed, payment completed, etc.) is a common approach, but there is a risk of accidentally overlooking those flags or getting the logic wrong.
Constraints by Types Using types to clearly restrict operations based on the state is a more robust way to prevent mistakes. By using different types for each state, incorrect operations can be prevented at compile time, reducing unexpected issues.
Concrete Example of Changing Aggregate Types
By assigning different types to a shopping cart for each state, specific operations can be restricted.
In-Selection Cart
Possible operations: Add products, remove products, enter payment information, confirm order
State: The customer is selecting products and putting them into the cart.
Post-Order Cart
Possible operations: Check inventory, final order confirmation
State: The customer's order is confirmed and finalized based on inventory, etc.
Paid and Pending Shipment Cart
Possible operations: Prepare for shipment, coordinate with shipping company
State: The customer has completed payment and is waiting for the products to be shipped.
Shipped Cart- Shipped Cart
Possible operations: Shipping notification, providing tracking information to the customer
State: The product has been shipped and the customer has been notified.
Received Cart
Possible operations: Accepting reviews, handling refunds
State: The customer has received the product and is in the stage where after-sales service is possible.
Ensuring Consistency through Types
By changing the type of the aggregate, operations are limited for each state. For example, after the cart has changed to a "Post-Order Confirmation Cart", the product addition functionality is completely disabled. This prevents products from being accidentally added and maintains consistency.
In this way, by varying the type of aggregation for each state, it is possible to realize business rules in a more natural form while ensuring the consistency and safety of the entire system.
Flexibility Brought About by Functional Programming
By utilizing functional programming, new flexibility arises in the state management of aggregates. In particular, an immutable data model is often used, and the approach of building up the state of an aggregate through events is naturally adopted. As a result, it is characteristic that state transitions and event applications can be described simply through functions.
State Transitions of Functional Aggregates
In functional programming, state changes are expressed as follows:
Aggregate' = EventType.OnEvent(Aggregate, event)
In this way, a new aggregate state (Aggregate'
) is created based on the event through the OnEvent
function. The important point is that the new aggregate (Aggregate'
) does not need to be of the same type as the original aggregate (Aggregate
). This allows the aggregate to have different behaviors and structures depending on its state.
Difference from object-oriented programming
Of course, it is possible to incorporate this concept in object-oriented programming as well, but in functional programming, it can be implemented more simply and intuitively.
In functional programming, logic that changes state is defined as side-effect-free functions, which clarifies the state management of aggregates and improves code readability and maintainability.
Applying Commands and Type Constraints
As mentioned in the previous section, the functional approach is very effective when implementing a mechanism where specific functionality is restricted by changing the type of an aggregate. Certain commands (operations) can only be executed on aggregates with specific states, and the functions that can be automatically applied change along with state transitions.
Example
For instance, the allowed operations differ between a cart with items being selected
and a cart after an order has been confirmed
.
When expressing this in a functional programming style,
addItem
is only applicable toCartSelectingItem
confirmOrder
is an event that occurs after the end ofCartSelectingItem
, and the type changes toCartAfterOrderConfirmation
By clearly defining the relationship between commands and states using types in this way, incorrect operations are prevented, and consistent business rules can be easily realized.
In functional programming, the type and state of an aggregate can be made to change naturally by events, and the flexibility to restrict operations accordingly can be provided. This enablesDomain logic management becomes simpler, allowing for flexible design while maintaining consistency throughout the entire system.
The Trend of Functional Event Sourcing
In fact, this type of subtype is already implemented in some event sourcing libraries. Sekiban also supports the Subtype feature.
It is also supported in the well-known event sourcing library, Axon.
Axon Framework : Aggregate Polymorphism
Aggregate Polymorphism in Axon Framework 4.10 is a mechanism that enhances reusability and extensibility by allowing multiple aggregates to have a common base class.
The base class implements common business logic, and each subclass can be designed to have different behaviors.
When Axon receives a command, it automatically routes it to the appropriate aggregate subclass and guarantees correct command handling. This enables a flexible and manageable aggregate design.
Furthermore, Oscar Dudycz, who has extensive experience in event sourcing, also writes the following in the article below.
https://event-driven.io/en/my_journey_from_aggregates/
In this blog he said following:
You can think about event streams as the story. In a good story, the main character transitions; Frodo didn’t start as a hero, aye? That’s also what’s happening with the entities in Event Sourcing. They’re usually state machines. Each state is actually a different aggregate. It has a similar set of data but different and the same with behaviours.
Some example? Sure, if you think about the room reservation on Booking.com, you could model it as a single aggregate, but actually, there’s a distinct set of operations you can do on the initiated, confirmed and completed reservation. You also have different data. The same can be noticed in others like the shopping cart, order, etc.
Sekiban supports subtypes in this way, but when designing aggregates with colleagues, I felt that subtypes alone were not satisfactory.
Subtype or Trait
When considering mechanisms where the type of an aggregate changes with state transitions, there are interesting options such as subtypes and traits. Here, we will explain how the type of an aggregate can flexibly change by comparing these two approaches.
Differences between subtypes and traits
Subtype is a design method where a class inherits from a parent class and inherits all the functionalities of the parent class. On the other hand, trait (or interface in C#) defines behaviors common to multiple types, and by implementing it, a class gains those functionalities.
Subtype approach
When using subtypes, you define a different aggregate type for each state and inherit from a parent class to inherit common functionality. This is effective if the states have common behaviors, but it may lack flexibility because all aggregates must have the same parent class.
Trait Approach
In the Trait (or interface) approach, you implement a different Trait for each change in the aggregate's state and implement the functionality corresponding to each state. The important point is that you don't need to have a common parent type for each type, so you can implement only the Traits needed for each state.
Concrete Example: Shopping Cart Aggregate
Here, let's explain using the shopping cart example from earlier.
Using subtypes
For example, when the state of the cart changes from "item selection in progress" to "after order confirmation", using subtypes allows considering the following inheritance hierarchy:
BaseCart (parent class)
SelectableCart (cart during item selection)
ConfirmedCart (cart after order confirmation)
With this approach, common processing is described in BaseCart
, and SelectableCart
and ConfirmedCart
each implement their own specific processing. However, in this case, all carts must be derived from BaseCart
, which tends to make the inheritance relationship more fixed.
Using Traits (interfaces)
On the other hand, with the Trait approach, you implement the necessary Trait (interface) according to the state of each cart. For example, you can define it as follows:
ICart (common interface)
ISelectable (Trait implemented by a cart during product selection)
IConfirmable (Trait implemented by a cart after order confirmation)
IPayable (Trait implemented by a cart in a payable state)
This way, when the state of the cart changes from "product selection" to "after order confirmation," the type changes from SelectableCart
to ConfirmedCart
, but each implements only the Trait corresponding to its state. In this case,
There is no need to depend on parent types, and behavior can be flexibly defined for each state.
Implementing Traits in C
When using C# like Sekiban, although the concept of Traits does not directly exist in C#, this concept can be realized by using interfaces. C# interfaces, like Traits, define common behaviors, and by having each aggregate implement them, flexible state transitions are made possible.
The subtype approach is based on inheritance relationships and inherits common behavior from parent types, so it is effective in simple cases, but when complex state transitions and diverse behaviors are required, it may lack flexibility.On the other hand, by using Trait (interface), it is possible to loosen the type constraints and implement different behaviors for each aggregate state, enabling flexible design according to state transitions.
What we want to achieve with Sekiban
We have already implemented the Subtype-based approach in Sekiban, but we are considering adding the more flexible Trait-style event sourcing functionality described above to Sekiban. We are currently thinking about the detailed specifications mentioned above. Until now, it was possible to retrieve all data within an aggregate type associated with an aggregate type and perform projections within the aggregate type, but we believe this will be difficult with this approach.
I've looked into various frameworks, but I haven't found many libraries or frameworks that include trait-style features like the ones mentioned above. To those of you with event sourcing experience, if you don't mind, could you share your thoughts? Event Store and others have the concept of event streams that are independent of the Aggregate type, but I don't get the sense that they go as far as considering consistency when stating and summarizing the functionality of type transitions.
Eventuous Function Services
https://eventuous.dev/docs/application/func-service/
Marten's event handler description style
https://wolverinefx.net/guide/durability/marten/event-sourcing.html
Equinox's F# event sourcing
https://github.com/jet/equinox
Commanded in Elixir
https://github.com/commanded/commanded/blob/master/guides/Aggregates.md#example-aggregate
However, there are many programming languages that have incorporated functional programming features, but I feel that there are not many resources that explain well the concept of flexibly performing type transitions. If anyone knows of any articles or implementation examples about event sourcing aggregates that are not subtype types while still being type-restricted, I would be very happy if you could share them with me!
Subscribe to my newsletter
Read articles from Tomohisa Takaoka directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Tomohisa Takaoka
Tomohisa Takaoka
CTO of J-Tech Creations, Inc. Recently working on the development of the event sourcing and CQRS framework Sekiban. Enthusiast of DIY keyboards and trackballs.