A JS web app demoing some patterns around how to implement undo/redo functionality for complex transactional systems. This application was made as part of "Innovation Day" at Flex Rental Solutions.
During my time at Flex Rental Solutions, I have written plenty of CRUD features that tackle complex business problems. One of those features was an "Undo Scanning Processor" where inventory that had been previously scanned to that event could be "unscanned" or have those scans reversed. This was a complex feature to implement as there are many business rules and side effects that modify state when an item is scanned to an event. Due to the complexity of the state, and how we were allowing items to be "unscanned" days/weeks later, we ended up essentially reverse engineering the scan process. We made it so that this Undo Scan engine infers much of the changes that were made with the original scan in order to put the system back into "initial" state.
In one recent team conversation, the thought came up about supporting "do/undo/redo" actions for our document's line items. Interactions with these documents can range from simple "update the qty of this line" to complex workflows "copy this entire document and all it's line items into another type of document with different settings, change the status of that other document, send out an email."
When I started thinking of these sorts of operations, I thought of how challenging to implement the Undo Scanning Processor was. I had to wonder, what other patterns there might be other than making more processors like "Undo Workflow Processor" or "Undo Line Item Creation Processor" or "Undo Line Item Update Processor" engine... I wanted to explore other patterns around undo/redo that might be a different appraoch than what we've already done.
This project is intentionally basic and was primarily coded in 2 hours. Claude Sonnet 4.5 helped me bootstrap the initial look of the app and basic CRUD and DOM updates, then I implemented the undo/redo logic. Here's some of my takeaways:
- Capturing the proper steps to undo an action/command when it happens makes a lot of sense.
- The Undo instruction must be recorded when "doing/redoing" and the redo instruction is recorded when "undoing".
- These instructions are user/session specific. If I am undoing a series of changes, I don't expect to undo another user's changes.
- Persisting these changes can become really big. Do we only persist the last 10 changes for a user?
- Conflict resolution is a consideration. If the app is close to realtime collab (which Flex isn't really, but could be one day) what are the common patterns for users "breaking" each other's undo sequences? IE I edit a line, that edit is added to my undo "stack" then a user deletes that line. If I undo, what happens?
- This "undo/redo" feature might be seperate from Revision Histories, depending on implementation. If a Revision History was desired, it seems like that would be a seperate feature recording the entire state of the document at a given point. Although, Event Sourcing came up regularly in my research of this topic. One argumen was that if using Event Sourcing, then you get the "revision history" for free because you have all th events and can replay them up to any arbitrary point to set the document to a certain "state". However, if the app relies on constant calculations of current state, then pure event sourcing might be expensive in order to get a representation of the current state of the document. However, there might be something about keeping the mutable state of the document, and rather than capturing "full snapshots" at different time intervals, we could capture and package batches of commands in order to set a document to a certain state. So there might be "today's version" and "yesterday's version" which will take today's version and apply all the undo transactions in order to get to yesterday's version.
There is much more research and understanding needed in the established patterns in these areas.
