4
votes

I'm trying to setup a WebApp using the CQRS/ES architectures. I have defined a model entity, with Create and Edit commands/events. Commands are handled by the related Saga. Events are saved to a mongo Event Store, which is the immediately consistent store. Events are handled by denormalizers that write to the eventually consistent SQL store.

Now I'm facing a problem with should-be-sync CRUD operations from the WebApp: the user opens a page with a list of these entities, read from the eventually consistent SQL store. Initially it's empty. Then, the user compiles a form to add a new entity. The client performs an ajax call to a method that issues a Create command to the bus, and then returns void.

On successful callback (and it is, because issuing the command no problem) the client refreshes the list of entities. If the denormalizer still hasn't handled the entity Created event, and written to the eventually consistent SQL store, the page will still present an empty list. What I want is a way to make the method that issues the Create command wait for the denormalizer.

I read a lot of blogs and stuff and I'm getting the idea that such synchronization goes against the idea of using a bus... but such user operations NEED to be synchronous: how can the user be presented with "insert successful!" and then still see an empty list??

I hope to get an answer since this use case seems basic to me...

2

2 Answers

3
votes

A while back a wrote a blog post on this exact subject. I offered 4 possible solutions.

  1. Disable and refresh - The idea here is to disable the edit fields once submitted until a specified period of time has elapsed. I don't like this approach personally as it creates a bad user experience and still may not work depending on the time to complete the read model updates.

  2. Use a confirmation screen - This is ideal for an end of process screen. Like a 'thank you for your order' or similar.

  3. Fake It - This is a good option. It's based on the fact that failure to receive an exception or validation error indicates success. You can, therefore, assume the new state of the read model. I have used this approach in production and work very well. You will need to be careful with version numbers. It also offers an excellent user experience. If for some reason the operation fails you can always inform the user later. This approach also assumes you are thorough with you validations and checks within the domain. It also helps to keep the read model updates as trivial as possible.

  4. Polling - As has already been suggested you can use polling or subscriptions to the read model. I have used this approach as well but combined it with option 3. It works well with frameworks like React.

You can read the full post here: 4 Ways to Handle Eventual Consistency

3
votes

What I see as the problem here is that you have probably picked the wrong technology to process your commands if you need them to be synchronous...

Rebus is asynchronous by nature, and it sounds like your whole setup is built around this premise too.

I can come up with a couple of ways of making the asynchronous look synchronous, but first I want to point out that you should NOT have have this requirement in more than a few (very few) places before I would recommend you implement your CQRS without the asynchronous command processing (e.g. by using something like Cirqus which can wait for one or more specific views to catch up, or by hand-rolling it).

With that said - :) I have an idea on how you can make an asynchronous operation look synchronous: Simply by polling the read store until your change is visible!

You can make the detection of whether your change is visible more or less generic, ranging from simply polling until the created user has popped up, to providing a correlation ID of sorts to your command, which is then transferred all the way somehow to all (re)projections, which can then be spotted by the initiating client.

But please consider this a hack. Please see if you can somehow avoid the need to pretend to be synchronous, or consider if another technology would be better suited.