Optimistic UI, CQRS and EventSourcing

Optimistic UI, CQRS and EventSourcing
 
When developing highly loaded web applications for better scaling, a principle such as CQRS is often used. It says that the method must be either a command that performs an action, or a query that returns data, but not both. In other words, the question to the system should not change the answer. More formally, the return value can only be clean, with no side effects methods.
 
But for a good scaling of the API separation, read /write is not enough. It is necessary to separate the databases with which this API works. Here EventSourcing comes to our aid. He suggests that we store all the events of the system in one database, call it the EventStore, and build all the other databases and tables based on it.
 
The combination of CQRS and EventSourcing very much unties our hands in terms of load balancing within the system, the number of its nodes, the number of auxiliary databases, the use of caching and others, but at the same time complicates the logic of the application and introduces many limitations.
 
In this article, we'll look at one of the nuances of designing a client part for such a system - optimistic updates in the UI.
 
For the frontend take fashionable React and Redux. By the way, Redux and EventSourcing are very similar technologies.
 
Optimistic updates to the user interface are not so easy to implement, and CQRS and EventSourcing complicate the task even more.
 
How should this work? Let's figure it out step by step.
 
 
We send the command and, without waiting for an answer, the optimistic event in the Redux Store. An optimistic event will contain the expected results of the server. Also at this step, we store the current state of the data that event will change.
 
 
We are waiting for the result of sending the team. If the command fails, the event dispatcher rolls back an optimistic update based on the data that was remembered in the first step. If everything is good, then we do nothing.
 
 
We are waiting for a real event to arrive at the client from the bus. When this happens, roll back the optimistic update and apply this event.
 
 
How it will look in practice:
 
 
 
 
The success of
 
Failure
 
 
 
 
 

 

 
 
 

 

 
 
 
 
The code for the optimistic update is described as Middleware to the Redux Store:
 
const optimisticCalculateNextHashMiddleware = (store) => {
const tempHashes = {};
const api = createApi (store);
return next => action => {
switch (action.type) {
case SEND_COMMAND_UPDATE_HASH_REQUEST: {
const {aggregateId, hash} = action;
//Save the previous data
const {hashes} = store.getState ()
const prevHash = hashes[aggregateId].hash;
tempHashes[aggregateId]= prevHash
//Dispatch an optimistic action
store.dispatch ({
type: OPTIMISTIC_HASH_UPDATED,
aggregateId,
hash
});
//Send a command
api.sendCommandCalculateNextHash (aggregateId, hash)
.then (
() => store.dispatch ({
type: SEND_COMMAND_UPDATE_HASH_SUCCESS,
aggregateId,
hash
})
)
.catch (
(err) => store.dispatch ({
type: SEND_COMMAND_UPDATE_HASH_FAILURE,
aggregateId,
hash
})
);
break;
}
case SEND_COMMAND_UPDATE_HASH_FAILURE: {
const {aggregateId} = action;
const hash = tempHashes[aggregateId];
delete tempHashes[aggregateId];
store.dispatch ({
type: OPTIMISTIC_ROLLBACK_HASH_UPDATED,
aggregateId,
hash
});
break;
}
case HASH_UPDATED: {
const {aggregateId} = action;
const hash = tempHashes[aggregateId];
delete tempHashes[aggregateId];
store.dispatch ({
type: OPTIMISTIC_ROLLBACK_HASH_UPDATED,
aggregateId,
hash
});
break;
}
}
next (action);
}
}

 
Live, as everything works, you can see here:
 

 
Conclusion
 
Optimistic updates in the UI can greatly improve the responsiveness of your application. Although they should be used with intelligence and great care. In some cases, they can lead to data loss and complicate the understanding of the user interface. For example, an optimistic live picture under the photograph is good, and an optimistic form of payment is bad. So do not break the wood. Good luck!
+ 0 -

Add comment