Optimistic Updates and CQRS

Anton Zhukov
reSolve blog
Published in
3 min readFeb 6, 2018

One of the most promising approaches to building modern apps is combining Command Query Responsibility Segregation (CQRS) and Event Sourcing. CQRS uses separate Query and Command objects to retrieve and modify data, respectively. Greg Young, one of the masterminds behind CQRS, strongly encourages using this approach in his great articles and interviews at http://cqrs.nu.

CQRS-enabled development differs from the traditional approaches, which makes it harder to implement common patterns like Optimistic UI.

According to the Optimistic UI concept, we can start an extended operation and instantly update the UI: show the estimated data while the real operation is still in progress. In other words, once a command is sent, we emulate a server’s positive response and display the expected results before receiving the actual API response.

When a command fails, we roll-back the application’s state and display an error message in the UI. This approach makes the application more responsive and only revert the user’s action in the case of a failure.

Let’s take a look at a simple React + Redux example:

The “Optimistic Hash Calculation” Example

Live Demo: https://codepen.io/MrCheater/pen/KZreRo

The code works as follows:

  • Dispatch an optimistic action;
  • Send a command and wait for a real action;
  • If a command fails, stop waiting for a real action and dispatch an optimistic rollback action;
  • When a real action is received, dispatch an optimistic rollback action and apply the real action.

Optimistic Update (Success)

Optimistic Update (Failure)

Optimistic Redux Middleware

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);
}
}

Conclusion

Optimistic updates may improve your app’s UX by making it more fluent and straightforward. Although, this approach can be dangerous: in some cases, optimistic updates can lead to data loss if the user skips an error message. Also, frequent UI changes can be annoying. Optimistic UI is just a tool and like any other tool: it can either solve problems or create them depending on the usage. Use your tools wisely.

--

--