3
votes

Is there a place for value objects in an event sourced domain model?

Lets define a value object as an object with immutable state that guards its invariants and has no particular identifier.

An event sourced domain model in this context is a domain that is entirely or partially event sourced, meaning that its current state can be derived from applying all events that have occurred in the past. Events themselves are considered immutable, even over time.

Debate has taken place about the validity of using value objects within events - this question goes slightly further: Do value objects have a place in event sourced domains at all?

The (potential) problem with using value objects is that it becomes rather tricky to alter the domain in such a way that invariants are tightened.

An example of this scenario would be to have a Username value object, with the sole constraint that the name must be anywhere between 2 and 16 characters.

While this has been working well for some time, the business decides to only allow usernames of at least 5 characters. A migration period begins and users with names of less than 5 characters are asked to update their names.

Lets say the process was successful, correction events are applied and everyone is happy. We tighten the constraints on our Username value object to require at least 5 characters.

For a while everyone is happy, but then we discover a problem with the snapshots and replay all events.

We now face an exception from our Username object: by loading the historic data, we're breaking an invariant of our domain.

The rules of a value objects apply retroactively - does this make them inherently unsuitable for event sourcing? Would it be worth applying versioning of value objects? Is there a simpler way of avoiding such problems?

4
"The rules of a value objects apply retroactively" - I don't get that part. What do you think is specific to VOs in that regard that would make them different from entities/non-value domain objects? - guillaume31
The part about invariants reminds me of this blog post. - guillaume31
@guillaume31 Retroactivity in this context means that the rules would also be applied to data from events in the past. The previous events were valid with the old version of the value object, but the update in the value object makes the past events "illegal" in the eyes of the new code. Other objects don't have this problem unless they validate their state upon construction. - Stratadox
Validating integrity at instantiation is a perfectly valid and normal thing to do for entities... See codebetter.com/gregyoung/2009/05/22/always-valid - guillaume31
I suppose the question touches on all objects that validate their state when applying all past events. I mentioned value objects by name because those are the most common use case, and because they are the subject of the debate this question is derived from. - Stratadox

4 Answers

4
votes

I would say, that at the moment you redefined what Username means, and you don't migrate historical data somehow, you've essentially created 2 different Username meanings.

Because there are 2 different meanings of the word, you have to make it explicit in the code somehow. "Versioning" is one way, although I wouldn't use such a generic solution, there are different modeling options.

You could make it explicit that the history of a "username" is just that, a history. So for example create a HistoricUsername, which is the event-sourced object, even a value object if you want. And create a Username which is at all times the username with the most current rules, which is not persisted at all, but created from a HistoricUsername if it can.

Some people suggest sometimes to extract the "rules" from the object, and re-apply it later. That way the object itself is valid at all times and you can ask it to validate itself against rules that might change. I don't really prefer these kinds of solutions, but it's an option, and the Username would still be a value-object.

So the problem is not really that value-objects don't fit into event-sourcing, it's just that the modeling has to be more accurate.

3
votes

Do value objects have a place in event sourced domains at all?

Yes.

Is there a simpler way of avoiding such problems?

"Don't do that."

The problem you are describing is really one about messaging - if we make backwards incompatible changes to our messages, then things break.

(More precisely, you have a "Username" message, and you are trying to re-use that message with a new set of constraints that reject some previously valid uses of the message).

The answer is that you don't introduce backwards incompatible changes - instead, introduce new names that match the new requirements, and deprecated the old ones.

Which is to say, adding support for new messages, and removing support for the old messages, become two separately managed options.

Greg Young's book Versioning in an Event Sourced System dedicates some chapters to this idea. Also, Rich Hickey ends up touching on these important ideas in most of his talks -- I'd suggest starting from Spec-ulation.

The "value object", meaning that the type that the current implementation of the domain model uses to move the information around, is a separate concern from the messages. The data structures we use in memory don't need to be coupled to our serialization formats.

The representation of the information on the wire is distinct from the representation of information in memory, and that in turn is distinct from the abstractions that manipulate the information in memory.

The challenging thing is that, at the beginning of a project, you have the least amount of information about when the different representations are going to diverge.

3
votes

We've solved this in a slightly different way. By separating the public API of our value objects from the internal (domain only) API, we are able to evolve one without affecting the other.

For example:

public class Username
{
    private readonly string value;

    // Domain-only (internal) constructor.
    // Does not enforce constriants and can only be called within the domain.
    internal Username(string value)
    {
        this.value = value;
    }

    // Public factory method.
    // Enforces business constraints. Used by consumers of the domain (application layer etc.)
    // to create new instances of the value object.
    public static Username Create(string value)
    {
        // Business constraints. These will evolve and grow over time.
        if (value == null)
        {
            // throw exception etc.
        }

        if (value.Length < 2)
        {
            // throw exception etc.
        }

        return new Username(value);
    }
}

Consumers of the domain must use the static Create method to create a new instance of the value object. This factory method contains all of our business constraints and prevents an instance being created in an invalid state.

Inside the domain, classes have access to the internal (constraint-less) constructor. Since this does not enforce any business constraints, an instance of the value object can always be created in this way (regardless of its value). By using this constructor when replaying events we can ensure that historical data will always succeed.

The benefits of this design are:

  • A single class is used to represent the domain concept (no need for multiple classes, versioning etc.).
  • Business rules are free to evolve over time.
  • Historical data always works. A Username from a year ago is still a user name, even if our rules have changed.
2
votes

Although already answered I do find this an interesting situation.

I agree with others that the event data should be record-based and, therefore, nothing more than a data container that may be used to reconstitute the aggregate.

That being said when the rules change so does the domain. A major portion of domain-driven design is to capture as much of the domain (rules/structure) as is required. If this is the case should the changes in the rules not also be kept?

For instance, if we have a Username Value Object and it starts out with the 2 to 16 characters rules then that is coded as such:

public class Username
{
    public string Value { get; }

    public Username(string value)
    {
        if (value.Length < 2 || value.Length > 16)
        {
            throw new DomainException("Username must be between 2 and 16 characters");
        }

        Value = value;
    }
}

Now we get to 1 March 2018 and the rule changes. We can keep the rule around:

public class Username
{
    public string Value { get; }

    public Username(string value, DateTime registrationDate)
    {
        if (registrationDate < new Date(2018, 3, 1) &&
            (value.Length < 2 || value.Length > 16))
        {
            throw new DomainException("Username must be between 2 and 16 characters");
        }

        if (registrationDate >= new Date(2018, 3, 1) &&
            (value.Length < 5 || value.Length > 16))
        {
            throw new DomainException("Username must be between 5 and 16 characters");
        }

        Value = value;
    }
}

That is the basic idea. In this way we keep our "old" rules around as well. This may become quite a hassle but I don't have enough experience to say. Changing our rules retroactively may introduce some pretty tricky situation so I guess one would need to evaluate this on a case-by-case basis.

Just a thought.