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.
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
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:
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
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)[https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences/] 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.
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 | | :--- | :--- | :--- | :--- | :--- |
- 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
prev_stateto get the previous state (this is necessary in order to support a 2-step request-response pattern).
- You can use an internal field called
Updated 29 days ago