Event-Driven Architecture with TypeScript and RabbitMQ

Agus Richard
Nerd For Tech
Published in
8 min readMar 26, 2022

--

Let’s have hands-on practice to event-driven architecture with TypeScript and RabbitMQ

Photo by Gary Bendig on Unsplash

With the rise of microservices architecture, the question of how to send data between services will emerge. What kind of pattern we should choose, between the traditional request-driven pattern or the foremost event-driven pattern?

While the decision of which pattern we need to choose depends on our application. In this article, we’ll see some concepts behind event-driven architecture and its implementation built using TypeScript and RabbitMQ.

Basic Concept of Event-Driven Architecture

First, let’s have a real-life example of request-driven architecture.

Imagine that you’re in McDonald's and desperately want ice cream. As you might know, sometimes when you’re there, the machine is broken. Then you ask the waiter, “When the machine will be fixed?”. The waiter says “In a couple of minutes”. Then you get back to your chair, eating your burger.

Five minutes later, you ask “Is the machine fixed already?” and the waiter responds “No”. Then you get back to your chair empty-handed.

Author’s image

You ask the same thing several times and the waiter still responds “No”. Until 30 minutes later, the machine is fixed and you can get your ice cream.

If I was the waiter, I would terribly annoyed if someone asked me every couple of minutes. So, what is the best way to make sure that nobody will annoy me as the waiter, but the customer still will be satisfied?

What you need to do is tell the waiter to inform you when the machine is fixed, but until then you should wait and enjoy your food.

Author’s Image

Okay… So the first scenario is what we call request-driven. The customer asks the waiter and the waiter responds. But this scenario is not entirely preferable, right? That’s when event-driven comes in. The customer doesn’t need to ask the waiter every single time. Just tell the waiter to call him if the machine is fixed. Technically speaking, the customer is subscribing to the waiter, and the waiter will publish the event that tells the customer that the machine is fixed.

In the next section, we’ll see event-driven architecture in action.

RabbitMQ

Basics

In the previous section, we have a real-life scenario resembling event-driven architecture. But notice that the interaction between customer and waiter is done directly, without any middleman. So, in this section, we’ll have another component that serves as a middleman for event-driven architecture.

In general, there are three components composing event-driven architecture. The Producer that creates events, consumer that accepts events, and queue that serves as the middleman. It is a message buffer that accepts and forwards events.

https://www.rabbitmq.com/tutorials/tutorial-one-javascript.html

This is where RabbitMQ is put into place. RabbitMQ is a message broker that accepts and forwards messages.

You can think about it as a post office: when you put the mail that you want posting in a post box, you can be sure that the letter carrier will eventually deliver the mail to your recipient. In this analogy, RabbitMQ is a post box, a post office, and a letter carrier.

RabbitMQ makes sure that when the consumer is not available, there is a place to store the message. Imagine if producer and consumer are interacting directly, there will be some messages gone when the consumer is not available.

Setting Up

For this article, we’ll use cloudamqp to host our RabbitMQ service and use its free-tier instance.

Now, go to https://customer.cloudamqp.com/login to login into your account. But if you haven’t created an account yet, you need to sign up first.

After that, you’ll see this page. Then click “Create New Instance”.

Author’s Image

You don’t need to provide any credit card or required information since we’re only using the free plan. Then fill in your instance name and keep the plan to Litte Lemur (Free).

Now, choose a data center that is close to you.

Click “Review”, then “Create Instance”.

There you have it. But to start coding, we need a connection string.

To know the connection string, click the instance name. You’ll see something like this.

Because this URL is sensitive data, you need to keep it private. We’ll use this in the next section.

Let’s Write Some Code

Setting Up Project

This project consists of two microservices, auth for registering new accounts and notification to send email to a new user. For simplicity, I choose to overlook the implementation details of registering a user and sending an email. But you’ll see how the event is transmitted from one service to another.

Before jumping directly to the code, we need to set up our project. If you are curious about how to set up a web API using TypeScript, here is a good reference for you.

Essentially, we need two web servers, auth, and notification. Let’s start with setting up auth web server. In your current directory, create a folder named auth, then change directory into it.

You can initialize a new project by running npm init -y . It’ll create a file package.json .

Install TypeScript by running npm i typescript --save-dev , then change your project into a TypeScript project by running npx tsc --init .

After running the above command, you’ll have tsconfig.json . To make that file simpler, update its content into this:

Create a nodemon.json for development configuration. So, you don’t have to re-build your project each time you change your code.

Then update package.json ‘s scripts, into this:

With these changes, you only need to run npm run dev , to spin up a development web server.

That’s it for auth. Now you can do the same for notification service. Or just duplicate the first one and rename it into notification.

Coding

I assume now that you have two folders in your current directory, auth and notification. Let’s start with auth service.

Install some dependencies required for auth service by running:

// dev dependencies
npm i --save-dev @types/express @types/amqplib nodemon
// main dependencies
npm i express amqplib body-parser

A simple list of dependencies for a simple web server, right?

Now, create a folder src inside auth directory and a new file named index.ts . Then write some template code there.

I guess the above code is quite plain and self-explanatory. It’s a web server with only 3 endpoints, /register , /login , and / . You see, there are console.log statements, we’ll send messages from here later on.

You can do the same for the notification service, but the content of index.ts is like this:

Pretty much the same as auth, but it only has one dummy endpoint that gives you back “Hello World”. Very authentic indeed!

It’s time to create the producer code. Go back to /auth/srcdirectory and create a new file named producer.ts .

Here we have a function creator called createMQProducer , and it returns a function that we’ll use to send events/messages (look at lines 22 to 24 for its definition). Lines 4 to 21 where the connection to RabbitMQ is established.

Next, go back to /notificationdirectory and create a new file inside src folder named consumer.ts .

Now we have also a function creator called createMQConsumer. Just like its name, it returns a function that will listen to RabbitMQ and wait for incoming messages.

Pay attention to lines 19 to 31. This is where we’ll do something when accepting the message, and the message itself will be in JSON format like this.

{
"action": "REGISTER",
"data": {
... user's data
}
}

It’s time to update index.ts files in both services.

Update index.ts inside auth service into this.

Replace the value of AMQP_URL into AMQP URL you have earlier when setting up RabbitMQ service.

Note that in line 11, we run the function creator to get the producer function. Then we use it in lines 22 and 34.

Next, update index.ts inside notification service.

Do the same thing as before for AMQP_URL.

Here, we run the function creator for consumer in line 11 and run the returned function in line 13. Function consumer will listen to the incoming messages and run simultaneously with the webserver.

I’d say that this approach probably is not the best way since it’s better to run the consumer in a different process.

There you have it. Let’s run both services to test our project.

If you go to /register and provide email and password then hit it. Look at the notification service. There will be a printed console, showing that the event is transmitted from auth service and received in notification service.

With this simple example, you can play when the event is received, like sending an email when an account is registered, updating the last login time, etc.

Conclusion

We had talked about concepts of event-driven architecture and RabbitMQ, then built a simple project in TypeScript. Even though event-driven architecture can make our microservices loosely coupled and heavily used nowadays, still it’s not a silver bullet that you can use for all types of applications.

Tradeoffs of using event-driven architecture include there is no central place to control the workflow and rollbacks are complex, especially if you have distributed transactions.

Both request-driven and event-driven architecture have their own benefits and tradeoffs. There is also a choice of using a hybrid architecture based on application requirements. With that in mind, it’s still a good idea to consider using event-driven when you want a distributed system yet loosely coupled and more flexible.

Here is the complete code: https://github.com/agusrichard/typescript-workbook/tree/master/event-driven-article-material

For additional reference, here is a bit more complex example: https://github.com/agusrichard/typescript-workbook/tree/master/event-driven-microservices

--

--

Agus Richard
Nerd For Tech

Software Engineer | Data Science Enthusiast | Photographer | Fiction Writer | Freediver LinkedIn: https://www.linkedin.com/in/agus-richard/