Dustin Wheeler

Hey! I'm Dustin Wheeler. I am a software developer. I love exploring new domains. Huge fan of testing. You can follow my ranting and raving at @mdwheele

Prefer scalar properties for Domain Events

Posted on October 8, 2017 • 4 minute read

TL;DR If you're using Domain Events as an interchange format across systems (including storage), using scalar properties (rather than Value Objects) makes serialization of messages much less confusing and burdensome to maintain. If your programming language supports deep serialization of object graphs, then go to town. In my experience, many language-native serialization techniques produce message formats that are not readily consumable by other systems.

When implementing Event Sourced or otherwise event-driven software, you'll eventually come to answer the question, "What's in an Event?". Domain Events represent something important in the domain that has happened in the past. You'll tease these out of domain experts during discovery workshops. When it comes to implementing your events, there is one piece of advice I would share...

Make your event properties string, int, bool or ProductId all you want, but never hold reference to an Entity or Aggregate Root. Domain Events are immutable. They represent a fact that has occurred in your system. If your events hold reference to an Entity and the state of that Entity changes after transmission, the event is no longer immutable and it's value cannot be trusted by clients. Use scalar or Value Object properties for your Domain Events.

On top of that, prefer scalar properties to Value Objects to represent the internals of your events. When I was learning, I got on-board with using Value Objects everywhere I could. I went VO crazy. They are in my Events. They are in my Aggregates. They are in my dreams...

However, all values are not created equal. Consider the quintessential Address Value Object. If I had a CustomerMoved event that included the current Address as well as the previous, serializing that event for storage wouldn't be too bad. Address is a pretty simple value, composed of scalars (street, state, zip, etc.)

What about CustomerId, which is a Ramsey\Uuid? This Uuid is a complex value is implemented using a complex object graph. Attempting to serialize this object directly is a mess. This particular library affords us the ability to serialize toString and back, but if it didn't, we'd be in a pickle.

In an orchestration system I'm working on, I have a ProcessDefinition value object that is represented by a (possibly cyclical) directed graph made up of nodes and edges. Due to the nature of its internals, serializing an instance of this class means I have to consider cycle detection, different node types and more. In events where I need to have the value of the ProcessDefinition available, I de-structure its internals into a scalar form like so:

[
    'nodes' => [
        [
            'id' => 'ccbc859f-6907-4dd1-a5a3-1305f7bd16d8',
            'type' => 'source'
        ],
        [
            'id' => 'e7ebcded-5eb3-49ec-897f-2b2079737a84',
            'type' => 'task',
            'trigger' => 'input:create.volume'
        ],
        [
            'id' => '9593b71a-cfe4-42f9-85cf-bfefae16f062',
            'type' => 'sink'
        ],
    ],

    'edges' => [
        [
            'from' => 'ccbc859f-6907-4dd1-a5a3-1305f7bd16d8',
            'to' => 'e7ebcded-5eb3-49ec-897f-2b2079737a84',
            'guards' => [
                'sys.vol_exists(req.vol.name) == false'
            ]
        ],
        [
            'from' => 'e7ebcded-5eb3-49ec-897f-2b2079737a84',
            'to' => '9593b71a-cfe4-42f9-85cf-bfefae16f062',
            'guards' => null
        ]
    ]
]

When serialized and transmitted to a client, it is readily consumable (imagine this structure as JSON consumed by a front-end framework).

All that said, storing information this way does not mean that you cannot provide getters on your events that reconstitute the value objects represented internally as scalars. The public interface of the event may expose value objects while still affording easier serialization through keeping internals scalar:

class ProcessStarted implements DomainEvent
{
    /* ... */

    public function __construct(Uuid $processId, ProcessDefinition $definition)
    {
        $this->payload['uuid'] = $processId->toString();
        $this->payload['definition'] = $definition->destructure();
    }

    public function id(): Uuid
    {
        return Uuid::fromString($this->payload['uuid']);
    }

    /* ... */

    public function asJson(): string
    {
        return json_encode($this->payload);
    }
}

This gives us a bit of the best of both worlds. In our Read Model Projections, we get to work with Value Objects. Additionally, our Event Store serializers are much simpler. They just need to know how to serialize a nested array of scalars (which even the worst language-native serialization facilities can't mess up too bad).

Ultimately, most of the justification to prefer scalars is in an effort to simplify serialization. It's a classic trade-off between convenience and simplicity. However, by using our public API to maintain scalar internals while exposing Value Objects as getters, we achieve a reasonable balance.

Welcome to my blog!

I will be posting a lot of content over the next few months across a wide variety of topics including material like you've just read. I am an aspiring writer, so if you can get past my terrible grammar, I think I have a lot of valuable ideas to share with you. If you're interested, I would really like to reach out when I publish a new article! No spam, I swear.

Go back home