6 ms·
> Reversal is the most straightforward when the event is cast in the form of a difference. An example of this would be "add $10 to Martin's account" as opposed
by criticaltinker 5y ago
> Reversal is the most straightforward when the event is cast in the form of a difference. An example of this would be "add $10 to Martin's account" as opposed to "set Martin's account to $110". In the former case I can reverse by just subtracting $10, but in the latter case I don't have enough information to recreate the past value of the account.
I've used the event sourcing pattern with great success in the context of an exceptionally complex web application for my day job. The front-end would create an 'event' for every action the user took. This allowed us to seamlessly implement client-side undo/redo functionality with surprisingly little effort, a market leading feature our users really appreciated.
One key insight we had was the necessity of capturing the state change as a difference, as Martin explains. This turned out to be tremendously powerful. We were able to build a variety of other features on top including detailed analytics of how users were using our app, and session replay for training and debugging, among other things.
- sbazerque 5y agofrom 10000ft away that has the feeling of op-based CRDTs
- fennecfoxen 5y agoMore or less.
- swagasaurus-rex 5y agoEvent sourcing can be represented as a chronological list of changes, replayed so that any list index represents a state. How CRDTs work is different in different implementations. Some CRDTs use a timestamp to order all changes, thus making it essentially identical to an event sourced list. Timestamp based ordering may not actually be the most ideal, especially for low trust environments where the timestamp can be spoofed. Some apps might not last-write wins. In this case, timestamp largely becomes meaningless because there has to be some other way to resolve conflicts due to multiple clients making changes to the same piece of data. Some apps want one clear winner, other apps might want the two inputs to be merged, others (like git) explicitly ask the developer to resolve the conflict. In this way CRDTs can diverge significantly from an ordered list of changes.
- blt 5y agoThe article also says: > ... all the capabilities of reversing events can be done instead by reverting to a past snapshot and replaying the event stream. As a result reversal is never absolutely needed for functionality. However it may make a big difference to efficiency... When I implemented undo/redo, I found that replaying from snapshots was fast enough. I wasn't working on a database-backed app though. Implementing reversal is extra work and bug surface, so it should be subject to cost/benefit analysis.
- josephg 5y agoI find fuzz testing works great for things like this. Make a function which, given everything you know about a user's state, randomly generates an action that user could take (and the expected result). Then you can run your user model forwards and backwards through randomly chosen actions. If you support undo, check that if you play any change forward then backward, the resulting state is unchanged. I usually pair that with a "check" function which just verifies that all the invariants I expected hold true. Eg, check the user has a non-negative balance. It sounds complex, but you get massive bang for buck from code like this. 100 lines of fuzzing code can happily find a sea of obscure bugs.
- Jtsummers 5y agoAnd if your system isn't using event sourcing (where this sort of testing is relatively simple to add), you can also do model-based testing. I like this example as a demonstration from FsCheck using F# (for the test program) and C# (for the program under test): https://fscheck.github.io/FsCheck//StatefulTesting.html https://fscheck.github.io/FsCheck//StatefulTesting.html
- new_stranger 5y agoIf you have any examples of this I'd love to see them. Are you just doing a random pick of X possible actions N times?
- osi 5y agoWe're using jqwik's stateful testing to achieve this, https://jqwik.net/docs/current/user-guide.html#stateful-testing https://jqwik.net/docs/current/user-guide.html#stateful-test... It's essentially as you described.
- k__ 5y agoI had the impression idempotency was the way to go in distributed systems and not differences.
- shhsshs 5y agoThose two concepts are not mutually exclusive. Each change can be idempotent by marking some identifying information about the state it was trying to update. increase balance by 10 (it is currently 100) balance is now 110 increase balance by 10 (it is currently 100) <- duplicate event! event has already been applied balance is now 110 It can definitely break down at some point (maybe the user really did want to increase by 10 two times, they just clicked twice really fast?). But idempotency and change-based flow can live together.
- Salgat 5y agoThis is where CQRS comes in. Every unique action has its own command id, so if a user clicks submit twice, the same command id is being sent twice and after the first success all future ones don't affect anymore change, they just return automatic success since that command id succeeded in the past. Combine this with versioning, where you assert an event stream version on write, and now you're able to affect change on a known state that cannot have changed (and if it did change, your command would fail and cause no persisted changes server-side).
- valenterry 5y agoThat means commands have to be stored as well (at least for some time) in a non-eventual-consistent way?
- dharmaturtle 5y agoIf a command is successfully converted/persisted into an event, that event should contain the command's ID. When the command is replayed, the domain aggregate/event stream will contain that command ID, and will therefore reject/ignore the replayed command.
- 02020202 5y agoin my events i have a context object that holds all "before" values of the current event plus some additional info that might be useful for reactors. ES is awesome but they bring a lot of overhead so it does not make sense for every project. in the ends, it's like git, but with context(this is what adds the overhead).