6

Part of the Aggregate + Event Sourcing (A+ES) methodology feels uncomfortable. Why does it feel uncomfortable? (As I've read from other questions and the book itself, it's a new-ish way of thinking, and the literature may not be great.)

I think part of it is the separation between state ownership, and state transition source.

The Domain Model has an Aggregate. Clients can affect changes in the Aggregate by Application Services. Application Services are the direct client of the Domain Model. Application Services call methods on the Aggregate. The Aggregate can publish Domain Events. The Aggregate decides if a Domain Event gets published, and if so, which Domain Event gets published. Typically, the Event reflects some change to the state of the Aggregate; or, the Event reflects something that has happened. Some change has occurred, and an Event followed that occurence. The change happened-before the Event. The Event happened-after the change.

With A+ES, we reconstitute the state of an Aggregate from the Events that have occurred. When a new Aggregate instance is created, there are no Events to reconstitute the state from, so the Aggregate is a blank slate. When the Aggregate is commanded to do something by an Application Service, some change happens, and an Event is published that records that change. Later, when we want to use that Aggregate instance again, we retrieve the Event Stream for that Aggregate instance, and feed the Events into it.

This I think is the part that makes me feel uncomfortable. The Aggregate takes commands from Application Services, performs some action that results in a state change, and publishes an Event that a change has occurred. With A+ES, the Aggregate must also take Events that it had published previously, and handle them accordingly. It has to react to the commands that generate Events, and now also has to react to the Events that it generated previously.

The example code in IDDD (Implementing Domain Driven Design) encapsulates a separate State object for the Aggregate. The State object is then the one that is responsible for mutating the state based on Events. In this example, the Aggregate receives the Events by means of its constructor, but then completely hands off the Events to a State object created in the constructor. This is called out as separating the behavior and the state into two different classes, with the behavior class (the Aggregate) referencing the state class to make decisions, and only permitting the state class to be modified via Events. The state can be queried directly from the Aggregate (eg. state.Name or state.Id), but it cannot be changed directly by the Aggregate (there are no setter methods available to support state.Name = "..."; or state.SetName(...);). The only way to change anything about the State, is to give the State an Event, and let the State determine what happens with that Event. Here, it seems that the Event comes before the change. Instead of a state change triggering an Event, the Event triggers the state change. The command generates an Event, and later, the Event becomes a command. To me, it feels like an unfamiliar temporal decoupling, and an inversion of control. It almost feels like some sort of inverted invariant enforcement. Instead of the Aggregate making some change to the state, ensuring the invariant is enforced, and publishing an Event that the change was made (and implicitly, that the change satisfied the invariant), the Aggregate instead tells its state "perform whatever actions necessary to make this true/enforce this invariant".

A (maybe not great) example might be the difference between

void LockAccount() {
    AccountLocked = true;// invariant enforced here
    var event = AccountLocked(...);
    PublishEvent(event);
}

and

void LockAccount() {
    var event = AccountLocked(...);
    state.Mutate(event);// "perform whatever actions necessary for the
                        // 'AccountLocked' invariant to be enforced."
    PublishEvent(event);
}

Is there some fundamental concept that I've missed that is making this feel uncomfortable, or is my understanding correct, and the uncomfortable feeling I get is simply unfamiliarity with this design paradigm?

EDIT: To explain from the comments, nothing is inherently wrong about the second code example. It makes sense mechanically. Part of the uncomfortable feeling comes from the sequence of events. If we only had the Aggregate class, and it managed behavior and state, then it would be weird if we published an Event before the appropriate state change had occurred. I would expect the state to be changed (flags set, values changed, objects replaced, etc) before an Event reflecting that state change was created.

With A+ES, the Event is created before the necessary state changes (flags set, values changed, objects replaced, etc) have occurred in the object. In fact, the Event is entirely what drives the state changes.

I think I may have found a way to reconcile this though. It seems like Events immediately follow "decision points". A decision point seems like a point in time, or code, that something has been undeniably decided.

Let's say we had an Aggregate, that had one method, and calling that method could result in two different Events.

void DoSomething() {
    if (state.SomethingIsTrue) {
        var trueEvent = TrueEvent(...);
        state.Mutate(trueEvent);
    }
    else {
        var falseEvent = FalseEvent(...);
        state.Mutate(falseEvent);
    }
}

In this code block, we have two decision points, nicely corresponding to the if and else blocks. If something is true, we have decided what we're going to do next. If something is false, we also have decided what we're going to do next. In both cases, an Event is published after the decision point has been reached.

If we apply this to the LockAccount examples above

void LockAccount() {
    // decision to lock the account happened before now
    AccountLocked = true;// state is changed to reflect the decision
    var event = AccountLocked(...);// event reflects that a decision has been made
    ...
}

void LockAccount() {
    // decision to lock the account happened before now
    var event = AccountLocked(...);// event reflects that a decision has been made
    state.Mutate(event);// state is changed to reflect the decision
    ...
}

Looking at it this way, it's not the Event that drives the change, nor is it the Aggregate that drives the change, but it's the decision that drives the change. Regardless of which comes first, changing state and sending event, or sending event to change state, both happen only after a specific decision has been made. The Event then doesn't reflect that the state has been changed, but that a decision has been made.

5
  • Sorry, I don't get it. With this approach you describe your aggregate mutations with events, for me is perfect. With this approach you restrict the possible mutations in your domain so you have more control of them. For you what is the issue with the second example? Commented Dec 11, 2020 at 1:22
  • And this is a question, why you say that accountLocked = true; is an invariant? Commented Dec 11, 2020 at 1:26
  • @LeonardoMangano I suppose it isn't an invariant, but rather an effect, which is what lead me to think about it as more closely tied to an Event. To me, there's no inherent problem with the second example. I guess it's more about the driving factors. To me, it's more about the time relationship between the Event being created, and the state change causing that Event. Without having a separate State class, the state would be inside the Aggregate class. When the Aggregate is told to do something, by calling one of its methods, it does something. For me there's a natural tendency for Events to...
    – Zymus
    Commented Dec 11, 2020 at 1:56
  • ... represent things that already happened. Without a separate State class, it seems weird if we publish the event without making the appropriate changes to the Aggregate, which is why the second example feels so strange. It feels like we're saying something happened (the account was locked) before the aggregate reflects that fact (Event is created before Locked flag is set to true). I may have thought of a way to reconcile this in my head, by determining the "decision point" of an Event. I'll see if I can find a good way to explain it in the question.
    – Zymus
    Commented Dec 11, 2020 at 2:00
  • I think your last update is very clear, the event happened when you decided to create it based in a command, after some validations and based in some conditionals. After that event was created (happened) you have lot of reactions like changing the aggregate in memory, then in the DB and then publishing the event. I think this is a good way to think about this Commented Dec 11, 2020 at 2:43

2 Answers 2

6

Looking at it this way, it's not the Event that drives the change, nor is it the Aggregate that drives the change, but it's the decision that drives the change.

Well, of course; this is what we refer to as application logic and business logic. That's what drives the change. Some of the decisions will be made in the application layer, some within the domain services, some in the methods of the aggregate, depending on how you separated the different responsibilities. Once you have your decisions sorted out, you make a state change.

However, I think what's tripping you up is a misconception about what it means to make state changes in an event sourcing scenario:

void LockAccount() {
    // decision to lock the account happened before now --> THIS IS FINE
    // You can make further decisions here if necessary

    // PROBLEM: The idea is NOT to do this two-step thing. 
    // Managing the state change is NOT a responsibility of this method/class.
    //
    AccountLocked = true; // state is changed to reflect the decision
    state.Mutate(new AccountLocked(...)); // event reflects that a decision has been made
    ...
}

This piece of code is an expression of what you've wrote earlier:

Some change has occurred, and an Event followed that occurence.

But the event doesn't follow a change, the event is the change.

The whole point is that state changes are, on a high level, represented exclusively through events. En event is "something that happened" in the sense that it's not a command or a request. It's not even a notification about something else that the aggregate did. Conceptually the event stream is the source of truth, the event stream is the state; as soon as you commit an event the state has changed - that's the idea. State changes are expressed as events; an event is an immutable record of a change by virtue of being a direct representation of that change (it's not a log of something else you've done).

Of course, to work with the aggregate, you reduce the event stream to a single in-memory state object (you "replay" past events), but this state object is internal to the aggregate; in-memory state changes are encapsulated. Whether or not you have a separate State class, the Mutate method is internal to the aggregate (it's not visible to any client code), and the in-memory state itself is encapsulated behind that method. So even within the aggregate, even if it's all a single class, when changing the state, you have to act as if you don't have direct access to state variables; the only way to effect state change is to call Mutate. But that requires documentation, and a deeper understanding of the design, and developer discipline. You can instead bake this into your design by separating out the state into a distinct class, and having the Mutate method be your only "window" to state mutation. So what you get is:

// In Account
public void LockAccount() {
    // do some logic, if any
    
    // effect the change:
    // no other way to mutate state, all the setters are private
    this.state.Mutate(new AccountLocked(...));  
}

// In AccountState
private Mutate(IEvent event) {

   // dispatch based on event, change internal state...

    this.Changes.Add(event);      // track uncommited changes
}

So the state object is not just some helper object that sends out events - the state is where the in-memory state lives. There's no separate state data in the Account instance. The event stream is the source of truth, the in-memory state is your "working area".

If your business logic is organized around a lot of getters, then this separation might result in some boilerplate, but if you mostly follow the Tell-Don't-Ask principle (in other words, if you are writing OO code, and not procedural code using objects), than you won't have a lot of pass-through getters.

P.S. Just for clarity, note that the word "aggregate" in DDD doesn't mean an aggregate of events; it's an aggregate of collaborating objects (including the case where the "aggregate" is only a single object), encapsulated behind an aggregate root (AR) - a particular object that provides the interface to the whole aggregate. So the aggregate here is { Account(AR) + AccountState }, with account state being internal to the aggregate.

1
  • What a great answer to a great question! Commented Aug 14, 2023 at 20:39
2

When and how events behave in event sourced systems depends on the context.

For event sourced aggregates themselves, the events happen:

  • after business rule validations,
  • before applying the event onto the aggregate itself.

The apply function itself modifies the aggregate's data, taking information from the recorded event and applying it on the record. The reason for this is that with event sourcing you need to reconstruct an aggregate from events, which is usually a method as simple as this:

class EventSourcedAggregate {

    // some state

    public static EventSourceAggregate fromEvents(Collection<Event> events) {
        foreach (events as event) {
            this.apply(event);
        }
    }
}

So if you have an aggregate which can be locked:

class Door {

    private UUID uuid;
    private boolean locked = false;

    private Collection<Event> recordedEvents = new List();

    public void lock() {
        if (locked) {
            return;
        }

        this.recordAndApply(new DoorLockedEvent(this.uuid));
    }

    private void applyDoorLockedEvent(DoorLockedEvent event) {
        this.locked = true;
    }

    private void recordAndApply(Event event) {
        this.recordedEvents.add(event);

        this.apply(event);
    }

    private void apply(Event event) {
        if (event is DoorLockedEvent) {
            this.applyDoorLockedEvent(event);
        }
    }
}

you don't want to change the aggregate's state in the lock method, because the method would never be called during aggregate's reconstruction.

And you also don't want the apply methods to contain any business logic, because some changes to your system since the time an event has been originally recorded could forbid the application of the event, which shouldn't be happening. Once an event has been recorded (and therefore passed the business rule constraints present at the time of the event creation), you should pretty much always be able to apply the event back onto the aggregate.

For other parts of the system, the events happen after the aggregate has been persisted (i.e. both after recording the event on the aggregate, as well as applying the changes to said aggregate using an apply method). Of course, for this you need some sort of infrastructure, which read the recordedEvents from the persisted aggregate and publishes them, so that other modules may listen to them.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.