Design Process

Process Overview

Often systems design will start with a high-level design and then describe key sequences. In this case, the framework abstracts away system design. Instead of focusing on architecture, the design process should instead focus on the business use case and work backward. It is important that during this process, there is no gap between the terms the business uses to describe what is being modeled and the technical representation. As such, the first step is to agree on common entity definitions and events.

The process described below follows an Event Storming, process but with a few standardized documentation steps.

Determine Eligibility

What problem are you trying to solve? You should be able to write a 1-sentence summary of what your service is supposed to do. Some examples:

  • "It tracks what's in a container"
  • "It manages orders for sales"
  • "It records when objectives were completed, who did it, and where"

Identify the Bounded Context

It's important to set domain boundaries on what your service will do. Do not attempt to model every piece of functionality inside the same system or use one set of states to cover a very large chunk of work.

As an example, consider an Order. An Order can mean many different things to many different people in a company depending on their role. Part of the work involved in modeling is the identification of users by persona, but that's just part of the story. "Order management" might mean tracking the process of capturing order details for a sales associate, but a few additional updates need to be made once the sale is made. Users in operations will work to fulfill the order and will therefore need to track many additional details. Lastly, an accounting user will care about payment terms and transaction status, but will again have a very different set of concerns than the fulfillment and sales users. As such, it may be best to represent "order management" as 3 separate systems. This is the approach commonly followed for high-volume trading systems in Finance.

Identify the Events

First, start by describing everything that happens to the entities in your domain. Don't worry too much about data representation at this point, just focus on capturing the real-world events that take place.

Document Any Commands that Caused the Events

Broadly speaking, there are 2 types of events: facts and requests. The key difference is that requests can fail, either due to validation failures or because of lifecycle state problems. From an implementation standpoint, this will mean needing to keep track of a RequestID so that the Command and subsequent Event can be tied back together.

As an example, consider the act of canceling an order. The cancelation could fail because of a missing Order number or because the Order has already been completed. Therefore a OrderCancelRequestCommand should be issued with an OrderCreatedEvent or OrderCreationRejected if validation fails. The important thing to keep in mind is that every request that could fail or succeed will require a similar 2-step process. This is most likely to occur in the following situations:

  • Creation
  • Permissioned-update
  • Delete

For very simple domains, adding 2-step handling may prove to be excessively burdensome. If you find yourself in this situation, consider whether event sourcing is the best fit for your system.

Identify the Actors

Every Event and Command need to have authorization roles associated. During the modeling phase, the goal is to track the main actors and create generic personas to represent classes of users. This role-level permission will be used to assign access rights during coding. Just as with any other project, core data protection principles apply. In particular, the principle of least privilege is essential to consider. Don't simply create an admin role with superuser powers.

Refine the Data Model

Once the high-level interactions and events are identified, data representation considerations can start to come into the picture. In many relationships, there are considerations of perspective. For example, consider an event-sourced service that tracks what container a particular package is in. It's possible to track the containerization operation (ie. putting one box inside another) from the perspective of the outer container, the inner container, or a combination of the two. Sometimes, however, there is an obvious right answer. Consider, a parent-child relationship towards containerization. If a parent container (ie. the outer container) were to keep track of a list of its child containers (i.e., the parentContainer has a children=[] array), then the updated pattern for adding a package to a container or taking items out is one of updating a large array. Modifying the contents of an array in a transactional fashion is certain to result in poor performance. As an alternative, consider changing the model by inverting the relationship. Instead of having the parent maintain an array of child containers, the inner container instead maintains a reference to its parent container. Changing the data model means that each operation can take place independently and can be trivially parallelized.

Next, consider whether other subentities are actually true entities or whether they can instead be represented as Value Objects. A (comparison chart)[] can help determine the difference. Essentially, Value Objects don't have a lineage and the values are therefore immutable. Many operations, like weight, volume, or currency comparisons can be best represented as Value Objects rather than as Entities.

Select Aggregate Roots

In order to improve maintainability and enhance logical coherence, an Aggregate Root is roughly the same thing as the point of the service itself. If the point of the service you identified in the very first design step is to "track vehicle onboarding" then the aggregate root may be a vehicle onboarding workflow ID. In some cases, the goal may be to track the lifecycle of a particular entity, such as a Container or Dispatch. in this case, Container ID or Dispatch ID are excellent candidates. It may still be the case, however, that breaking down the relationship further into smaller aggregate roots is desirable. Orders, for example, are better broken up along Bounded Context borderlines.

Design Artifacts

Before you start coding, make a table using the following format:

|Event or Command Name | Description | Actor | Preconditions | Postconditions |
| :--- | :--- | :--- | :--- | :--- |

Simply include this table and fill it out as part of the design process. When you're done and want to start coding, simply add the table to your project's GitHub documentation along with the following summary:

# Project: <PROJECT_NAME>
## Overview
This is an event system that <ONE_LINE_OVERVIEW>.

The aggregate root for this system is: <AGGREGATE_ROOT_FIELD>.

## Event Map:
|Event or Command Name | Description | Actor | Preconditions | Postconditions |
| :--- | :--- | :--- | :--- | :--- |

Design FAQ

  • How do I convert a state machine?
    • Start off with events that lead to state transitions. You should be able to recreate the same logic of the framework
  • How do I keep track of the state?
    • You can use an internal field called state along with prev_state to get the previous state (this is necessary in order to support a 2-step request-response pattern).