Event-Driven Architecture with TypeScript and RabbitMQ
Let’s have hands-on practice to event-driven architecture with TypeScript and RabbitMQ
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.
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.
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.
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.
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.
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”.
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.
How to Set up Development Server Using Typescript And Docker
Get rid of the daunting task of setting up your first development server with these easy steps
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
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:
nodemon.json for development configuration. So, you don’t have to re-build your project each time you change your code.
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.
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,
/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
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
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.
... user's data
It’s time to update
index.ts files in both services.
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.
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.
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
Event-Driven vs Request-Driven (RESTful) Architecture in Microservices
The 'Taxi-Ride' Interaction example Let's take a closer look at what a REST API is. It's basically an interaction…