Cancel (or Rollback) a number of Events

Hi all.

Suppose this scenario. A user invokes several commands on a aggregate, like

  • Create Process
  • Add Line
  • Add Line
  • Add Comment
  • Commit Work

Process will contain 2 Lines and 1 Comment. After a while it does some more work.

  • Restart Process

  • Add Line

  • Add Comment

  • Add Comment

  • Cancel Work

At this time, I need my Process to still have the same 2 Lines and 1 Comment and not 3 Lines and 3 Comments. However, the events are already there, so they must be true.

How can I achieve this? I know one way is to have a Saga sending compensation actions, like

  • Remove Line

  • Remove Comment

  • Remove Comment

But that will require lots’ of effort since for every action will have to be a counter-action, which may not always be possible.

Another way I thought it to manage manually the UnitOfWorks to span several commands/events, but this feels like opening a can of worms…

And still another way will be to use Snapshots. So the the process will be

  • Create Process

  • Add Line

  • Add Line

  • Add Comment

  • Commit Work

  • Create Snapshot

  • Restart Process

  • Add Line

  • Add Comment

  • Add Comment

  • Cancel Work

  • Reload Snapshot

This looks good in theory, but I never worked with Snapshooting, and I have several doubts about it, for instance, when we create a Snapshot the Events are deleted or they will stay in the EvenStore? How will Snapshooting play when replaying events? How can I make sure that external listeners won’t listen to the Events that were “cancelled”?

Any help much appreciated.

Cheers.

Hi Antonio,

I would definitely recommend against any workaround that involves the UnitOfWork or Snapshots. As you said, once events have happened, they should be permanent.

I’m not entirely clear on whether the second part of the work happens in a single command, or over a period of time. I’m going to assume that these are five different commands and five units of work.

  • Restart Process

  • Add Line

  • Add Comment

  • Add Comment

  • Cancel Work

The fundamental principle o the aggregate appears to be that it tracks “draft” data, and “committed” data. Data that’s draft should not be seen anywhere in the system except for the draft UI. Committed items can be used by other parts of the system (i.e. printed, emailed, totaled, dispatched, shown publicly…)

I would consider splitting up the data in the aggregate and to use different events to signify the transition. This could be as simple as a ‘status’ field on a Comment and Line

  • Process Restarted Event
  • Line Drafted Event
  • Comment Drafted Event
  • Process Committed Event
  • Line Committed
  • Comment Committed
    or
  • Process Aborted Event
  • Line Reverted
  • Comment Reverted

Either way you end up with more events, or additional data on the event. The ‘committed’ and ‘rolled back’ events might simply include references to the comments/lines that are being added/reverted… or each line/comment fires additional events when the line/comment status changes.

It’s definitely more work and more events to track all of this, but it sounds like these are legitimate events that happen in your domain. I’ve implemented “undo” functionality in a very similar way – the aggregate tracks which data was added as part of what action, and knows how to apply a compensating event that reverts that change if we need to roll-back the change later.
The explicit events are absolutely necessary for views and search tables to remain up to date.

~Patrick

Hi Patrick, thanks for your reply.

The scenario here is almost exactly of what you said, to have some kind if “draft” data that at some point is either committed or cancelled, in which case some “undo” or “rollback” like action must be done.

I do understand your objections to using a UnitOfWork, since that is a part of the inner workings of Axon and thus should be changed only on “structural” requirements and not on “business” requirements. However, in relation to snapshoting I don’t understand your reasons. Snapshoting is on the same side of Events (on the" business" side as opposed to “structural” side), so I don’t see why shouldn’t be used in situations like that. Unless I’m not understanding snashoping well, as I said I never worked with it.

Nevertheless your solution looks good, but I have two concerns:

First, it’s not general, it has to be coded for every type of actions that can be reversed, and that may not always be possible.

The second is, the way I see it, other parties will still be listening to the “draft” events, which means they still see things they shouldn’t see and act upon them. Is there a way for the UnitOfWork be finished without sending the events to the event bus, so nobody else could be aware of those “draft” events?

I’m going to implement this the way you suggested anyway.

Thanks for your input.

Cheers.

The snapshot mechanism is only intended to optimize the loading of aggregates with many events. The application should never know if snapshots were used. The domain event stream is the source of truth of the application, and it will contain events even if you were to replace the current snapshot in the snapshot store with an earlier version. This type of intervention in the aggregate repository essentially “corrupts” the aggregate by loading an out of date version. This is similar to a user working in the system with a stale version of the aggregate on the screen.There are many technical issues with this approach:

  • Events have already been sent to event handlers. They must be compensated for, and simply messing with the snapshot does not accomplish this.
  • Snapshots can be discarded and rebuilt (by replaying all events in the domain event stream.) This would apply the changes that were “rolled back” by a subsequent user action.
  • Deleting and rebuilding snapshots is actually a fundamental feature of Axon. The classes that implement your aggregates (as well as the classes that implement your events) can change from release to release. Axon provides an “upcaster” facility for domain events, but there’s no such feature for snapshots. Any change to the domain logic (e.g. adding an event listener to store data that wasn’t previously stored) inherently requires that snapshots of the aggregate from a previous application version must be discarded as they old serialized state must not be applied to the newer version of the class.

It’s true that it requires many more events to deal with granular do/undo functionality, but I think that’s an inherent part of the problem domain (rather than incidental complexity added by event sourcing.) Event sourcing simply makes it more obvious what’s happening behind the scenes. An implementation using object-relational mapping to store state would have to deal with similar complexity: Commited and draft copies of the object must be stored, and the system must be able to delete the draft rows/objects on rollback, and promote them on commit.

It might be possible by getting away with fewer ‘state management’ events by taking care of the state at a higher level. Instead of having draft lines and comments, they could be grouped into "DocumentVersion"s:

  • Draft initiated Event

  • line added

  • comment added

  • Draft committed

  • Draft initiated event

  • line added

  • comment added

  • Draft cancelled event

Of course, the work for e.g. a UI view event handler doesn’t really change much. Like I said, I think that much of this is inherent complexity of building a transactional workflow w/ commit or rollback.

~Patrick

Hi again, thanks for your quick reply.

OK, I understand now that Snapshots are not what I thought they were, and they are in fact part of the “structure” of Axon like the UnitOfWork and not of the “business” layer of the application.

So, going forward with the solution you suggested, I still have the problem of having external listeners eventually listening to the “draft” events and that seems dangerous. Is there a way to make some “types” of events (“draft” events) to be applied on the aggregate, persisted as Event Sourcing but not be published to the event bus at the end of the UnitOfWork?

If this is possible this solution will be both functional and secure, so it will definitely be the way to go.

Thanks again for your help.

Hi Antonio,

you probably want to make the concept of “drafts” explicit in your events. When completed, you can “commit” a draft. Optionally, the commit could provide a summary of all the data in the committed entry.

Cheers,

Allard

Hi Allard,

I have a similar problem where the maker/approver pattern is followed. Only the events of the approved transaction should be published to the external consumers whereas the draft events should be limited to the query portion of this entry service. I am storing the copy of the draft and approved entities in the event sourced aggregate and reverting back to the last approved version if rejected by the approver.

Could you please advise whether it’s the right approach or it can be done in some other effective way.

Regards,
Rajesh

Hi Rajesh,

it sounds all right, as an approach. Obviously, you’ll need similar logic on the command and query side here, to reflect the “draft” and “approved” state of things. In one of the projects I was involved in, my customer came up with the concept of a value object that would store current state. Based on actions, you could generate a new value object that would reflect the new state. The aggregate could use this object to make decisions (and generate events). Certain query modules could use this to reflect the changes described in events in the query model’s state. Of course, the value object would also expose its state.

To ensure only approved changes are published to external consumers, I would use a different message source for external components. A module in between could stage (and/or translate) events and only forward them once a certain state has been approved. This way, you prevent external components from needing to know about the internals of the approval process.

Hope this helps.
Cheers,

Allard