I originally thought that putting the undo information directly in the events would be a fine solution to store them away in a consistent way. But now I see two contradicting factors:
- In an editing application, a single user change may consist of multiple smaller changes. Imagine a user deletes a number of selected nodes. There are two ways to model this change. Either create an new event named “DeletedNodes”, which directly supports that change, or bundle a number of simpler “DeleteNode” events together in one change set. I prefer the latter, because it is more modular. It should be possible to group simple changes together to form an larger one that represents an atomic unit. So I guess we need change sets anyway.
- I recognized that the undo information is not always required and may just be an annoying burden to carry with. For example, in replication, branching or merging scenarios (yes, I want to cover this, at least in the design).
So the undo information should be logically separated from the change events.
Now knowing that the forwarding event and the compensating event need to be separated, I want to generate the compensating events automatically. And actually, this does not seem to be so hard after all.
Reconsider what we have right now:
- A store for the forwarding change sets which is linked to another store for the compensating change sets.
- The domain model in the database, which can always be rebuilt by the forwarding change sets we stored.
- A memory copy of the domain model for the user interface.
Given that the database’s copy of the domain model is synchronized to the change set store (storing change sets and the update of the model are processed in the same transaction), the previous state of the domain model is available at the time we generate the forwarding events. So there is a function that can derive a compensating event from a forwarding event by querying the unchanged domain model in the database.
This leads us to the steps that are required to store a revertible change set and to simultaneously update the domain model:
- Generate a forwarding change set.
- Begin the Transaction.
- Generate a compensating change set by generating a compensating event for each of the forwarding events in the change set. Previous information is queried from the current state of the domain model, which is unchanged yet.
- Store both change sets.
- Apply the forwarding change set to the domain model.
- End the Transaction.
Sounds simple, especially now that the compensating events can be built on the same types the forwarding events are made of.
Some points to note:
- Change sets are not allowed to contain conflicts. For example, if a new domain object is created, a destructive event for the same object is not allowed in the change set. But this is a constraint that is usually not violated in a user interaction.
- The user interface already accesses in-memory domain objects and modifies them on the go (for example, when the user is in the process of moving a node), this may lead to inconsistencies when the transaction that confirms the move fails (which it shouldn’t, because I plan this to be a single client / server model; the transactions are just to ensure the consistency within the persistent storage).
So, I guess the implementation won’t be that simple. I’ll report on that.
Update: Though it took me over a week to get to it, the implementation was straightforward and took only around 2.5 hours. The concept is sound: the compensating change sets contain all undo information and the domain model always stays current.