On responsiveness and performance in event-driven services

Posted on Mar 14, 2022
Note: This article was written a while ago and may contain outdated information. Please verify the details before relying on it. If I express opinions or recommendations, they might not reflect my current views. For this reason, I recommend checking for more recent articles on the same topic.

… or: why it is better to create jobs instead letting the user wait.

One unique characteristic (or also a nice win) in event-driven architectures is that it gives us the capability for asynchronous communication.

You can send an event and just don’t care what happens with it (fire-and-forget) or send a request and wait for the reply. In the latter one event-driven architectures help a lot to increase the responsiveness of an application.

Example: Creating reports

Consider some long-running operations like creating a report out of a lot data: The user sends a request to the service to create a PDF file with all the data.

The service itself needs some time to fetch the data, check the data, do some operations on the data and render the PDF.

I worked on a system where exactly this happened in a non-event-driven style. The creation of such a report took from 2 seconds up to 10 minutes - depending on the complexity of the data and report. When the user wanted to generate such a report, he was firing a button in the UI and had to wait for the response from the backend until it was generated (which means the user was waiting up to 10 minutes).

Then we started to do a rewrite of it in an event-driven style.

Responsiveness and performance

Beside of doing a rewrite of the whole application to a event-driven style, where the service created jobs and did all operations asynchronously, we started to let the user know that a job for generating the report was created.

So as soon as the user hit the button and the job information was placed in the application, he got a response that the job was placed. You can think of it like placing an order at the stock market.

This led to a better UX for the user as he didn’t have to wait for x minutes until he was sure that his click was ok. So the responsiveness was improved a lot. Furthermore the user didn’t click the button multiple times anymore, which was a huge issue in the service because it didn’t have some sort of check-up if the same job already exists.

An example which doesn’t make use of event-driven architectures is shown in the image below:

The user clicks the button in the UI and the UI triggers the Generator.

The Generator then adds the Job to the Queue and responses immediately to the User that the Job is in the Queue.

The Queue updates the status to the StatusController (with status like wait, run, etc.).

As soon as the Processor is ready it fetches the Job from the Queue and starts the generation.

Diagram of an event-driven job processing pipeline with a queue and processor

With moving the synchronous monolithic service to an event-driven service which does all sub-tasks in parallel if possible we also increased the performance.

Error handling

When the user created the job a database entry was created which had the following structure:

job_id status

Beside of this there were tables with more details about the job, but the one above stored the current status of the job.

With this the user always could have a look at how far the job is and if an error appeared. Which is really important, so that the user either can fix it or contact a support person for more information.

This led to a more error-resilient system and gave the user the possibility for self support.

Another approach would be to implement a further step in the generator which tries to fix the problem by itself.

Also a websocket connection could be helpful, so that the user can see the current status without a long-polling or a ‘refresh manually every 5 seconds approach’.

Want to know more?

Keep on reading and choose one of the related articles. You can also check the home page for my latest thoughts, notes and articles.