Modeling in Tracepaper: The Aggregate

Modeling in Tracepaper Oct 06, 2022

We continue the Modeling in Tracepaper series by focusing on the hearth of our application. The Domain logic is encapsulated in aggregates. The scope of this post is visualized in our concept overview.

What is an Aggregate? An aggregate is a concept borrowed from Domain Driven Design. We used 3 core concepts regarding the aggregate:
1. An aggregate instance contains an identity and holds an internal state.
2. An aggregate defines a transactional boundary. This means any changes to the aggregate will either all succeed or none will succeed.
3. An aggregate represents a domain concept.
It is quite literally an aggregation of stateful behavior to represent a domain concept.  

A commonly used tactic is to model an aggregate as a cluster of associated entities and value objects treated as a unit for the purpose of data changes. Data changes are dictated by rich domain behavior, unlike CRUD-style applications.
At Draftsman, we use an alternative tactic for state persistence and manipulation, Event Sourcing.

In short, an aggregate in our modeling tool Tracepaper will contain:
1. a definition for the state, modeled as aggregate attributes.
2. domain event definitions, a domain event may mutate aggregate attributes (among other things.)
3. Behavior Flows, a behavior flow is a sequence of logic functions, triggered by an event, and may result in the publication of domain events.

An aggregate can only interact with the outside world by receiving and publishing events. For example, an aggregate can't use API calls to fetch data or send a mutation to another aggregate. All data needed for the behavior flow must be encapsulated in the trigger event, subsequently, other aggregates can subscribe to the event stream.

We do have a concept of Notifiers that are able to interact with the outside world. Just as an aggregate it is triggered by an event, whereafter it can orchestrate API calls to other systems or the application API itself. Unlike an aggregate, a notifier does not hold any state. But this is a subject for another post.

The Subdomain

An aggregate is not free-floating in our domain, in Tracepaper we support a concept called Subdomain that acts as a cluster of related aggregates. A subdomain is a bounded context within our domain. So before we can start modeling an aggregate, we have to initialize a subdomain.

The Aggregate

Let's say we have a subdomain accounting and we want to model a concept Account that is used to store the account balance. It has behavior for:
1. Opening an account.
2. Deposit into the account.
3. Withdrawal from the account.

We start with an empty aggregate. The first thing to do is to add a state definition.

Our state definition is very simple, but could definitely be more complex. The next thing to do is start modeling behavior, let's start with the behavior to "Open an account". Let's say we have an additional requirement when opening an account that we will reward everyone with a 10-credit addition to their balance as a welcome gift. With this in mind, it is important that the mentioned behavior is only applicable on non-existing accounts, read, and can only run during aggregate-instance creation.

We have initialized a behavior flow. A behavior flow is a chain of processors that together define the behavior. We have access to the internal state of the aggregate, although this is limited to read-only. So we can't manipulate the internal state, so let's create some flow variables that we can use as temporary storage.

Tracepaper provides "best effort" auto-filling fields. It saves you some typing, but you are not absolved from thinking... double check everything.

A behavior flow is always started from a trigger, a trigger is always an event. We have 2 types of events in our domain.
1. Domain-Events, emitted by aggregates. These will be introduced in this post.
2. Actor-Events, these are triggered by commands and can be triggered via an API call. An actor event always follows the "{CommandName}Requested" naming semantic.

For this flow we want to use the second type of event, this means we have to model a command. Modeling the command is a subject for another post. So for now, let us just assume that the flow variables emailAdress and balance are filled at the start of the flow with aid of the trigger mapping.

We skip ahead to the processors, here we can model the requirement to add the 10-credit gift. We do this by adding a "set-variable" processor.

We selected "balance" as a target and provided the expression "flow.balance + 10". The expression is evaluated as python, because that is the language we selected for our applications. When you generate source code you will find a line:

flow.balance = flow.balance + 10

We stated that you have access to the internal state of the aggregate, but issuing the next statement is not gonna do it.

flow.entity.balance = flow.balance

This statement will execute successfully, but it won't be persisted. To update the internal-state we have to emit a domain-event.

A domain event is an attribute of the aggregate and could be emitted from multiple behavior flows, it is not bounded to a specific behavior flow. This means we could create one from the aggregate screen. It is, however, more convenient to create one from within our flow. This does not change the positioning of the event, it is still bounded to the aggregate, but we will be supported by the "best effort" autofill functionality.

A little side step, maybe you noticed that I had to refresh the page during the demo. The thing is, we have an event-based architecture. Even the communication between the front-end and back-end is a mix of synchronous and asynchronous communication. This means everything is eventually consistent.
In the backend, we have something called the transactional outbox and various retries to protect us from missing events. The front-end, however, may sometimes miss an event, meaning that your projection is not consistent with the application state. Luckily we can just refresh the page with F5. Most of the time this will fix the issue. If not, you could click  "Tracepaper" in the upper left corner this will ignore any cached data.

A lot happening on the screen, let's break it down.

The first thing we did is model a domain event. A domain event has an internal name (Opened) that is unique within the aggregate and an external name (AccountingAccountOpened) that is unique within the domain.
It has fields, and those are the attributes that are persisted in the event log. There is also a handler-mapping. The handler-mapping defines how the event is applied to the aggregate... It is a setter.

The event is added to the aggregate view, and from here you could edit it.

You can choose to edit only the mappings.

Or you could edit the fields and the mappings. Let's say we change the balance from Int to Float.

This will result in a new event version to ensure backward compatibility, events are persisted after all and you may already have events in your log when you decide an additional field is needed in the event. We don't want our software to crash because we have trouble replaying the event log to determine the state.

Emiting the Domain Event

The second thing we did was model the emit-event processor. It is pretty straightforward. We select the event we want to emit (AccountinAccountOpened) and we map flow variables to event fields. This is best effort auto-filled, but at this point, my browser became out of sync and needed a page refresh.

Tracepaper backlog regarding the Aggregate.

A good time for a break. The only 2 things left for this behavior flow are a trigger and a test. As mentioned, we want to trigger from a command and that deserves a separate post. Modeling the test without a trigger is not possible, so this has to wait. Thnx for reading, until next time.


Bo Hanssen

I've been a technology-agnostic developer since 2015. And currently filling in the role of solution-architect for one of the core business-systems at APG.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.