Usama Ashraf
8 Nov 2018
•
4 min read
Before event-driven programming became popular, the standard way to communicate between different parts of an application was pretty straightforward: a component that wanted to send out a message to another one explicitly invoked a method on that component. But event-driven code is written to react rather than be called.
This approach causes our components to be much more decoupled. Basically, as we continue to write an application we’ll identify events along the way, fire them at the right time and attach one or more event listeners to each one. Extending functionality becomes much easier since we can just add on more listeners to a particular event without tampering with the existing listeners or the part of the application where the event was fired from. What we’re talking about is essentially the Observer pattern.
Identifying events is pretty important since we don’t want to end up having to remove/replace existing events from the system, which might force us to delete/modify any number of listeners that were attached to the event. The general principle I use is to consider firing an event only when a unit of business logic finishes execution.
So say you want to send out a bunch of different emails after a user’s registration. Now, the registration process itself might involve many complicated steps, queries etc. But from a business point of view it is one step. And each of the emails to be sent out are individual steps as well. So it would make sense to fire an event as soon as registration finishes and have multiple listeners attached to it, each of which is responsible for sending out one type of email.
Node’s asynchronous, event-driven architecture has certain kinds of objects called “emitters” that emit named events which cause functions called "listeners" to be invoked. All objects that emit events are instances of the EventEmitter class. Using it we can create our own events.
Let’s use the built-in events module (which I encourage you to check out in detail) to gain access to EventEmitter
.
// my_emitter.js
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
module.exports = myEmitter;
This is the part of the application where our server receives an HTTP request, saves a new user and emits an event accordingly:
// registration_handler.js
const myEmitter = require('./my_emitter');
// Perform the registration steps
// Pass the new user object as the message passed through by this event.
myEmitter.emit('user-registered', user);
And a separate module where we attach a listener:
// listener.js
const myEmitter = require('./my_emitter');
myEmitter.on('user-registered', (user) => {
// Send an email or whatever.
});
It’s a good practice to separate policy from implementation. In this case policy means which listeners are subscribed to which events and implementation means the listeners themselves.
// subscriptions.js
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
// send_email_on_registration.js
module.exports = (user) => {
// Send a welcome email or whatever.
}
This separation allows for the listener to become re-usable too i.e. it can be attached to other events that send out the same message (a user object). It’s also important to mention that when multiple listeners are attached to a single event, they will be executed synchronously and in the order that they were attached.
Hence someOtherListener
will run after sendEmailOnRegistration
finishes execution.
However if you want your listeners to run asynchronously you can simply wrap their implementations with setImmediate
like this:
// send_email_on_registration.js
module.exports = (user) => {
setImmediate(() => {
// Send a welcome email or whatever.
});
}
Stick to the Single Responsibility Principle when writing listeners: one listener should do one thing only and do it well. Avoid, for instance, writing too many conditionals within a listener that decide what to do depending on the data (message) that was transmitted by the event. It would be much more appropriate to use different events in that case:
// registration_handler.js
const myEmitter = require('./my_emitter');
// Perform the registration steps
// The application should react differently if the new user has been activated instantly.
if (user.activated) {
myEmitter.emit('user-registered:activated', user);
} else {
myEmitter.emit('user-registered', user);
}
// subscriptions.js
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
const doSomethingEntirelyDifferent = require('./do_something_entirely_different');
myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
myEmitter.on('user-registered:activated', doSomethingEntirelyDifferent);
In the previous example our listeners were totally independent functions. But in cases where a listener is associated with an object (it’s a method), it has to be manually detached from the events it had subscribed to. Otherwise, the object will never be garbage-collected since a part of the object (the listener) will continue to be referenced by an external object (the emitter). Thus the possibility of a memory-leak.
For example if we’re building a chat application and we want that the responsibility for showing a notification when a new message arrives in a chat room that a user has connected to should lie within that user object itself, we might do this:
// chat_user.js
class ChatUser {
displayNewMessageNotification(newMessage) {
// Push an alert message or something.
}
// `chatroom` is an instance of EventEmitter.
connectToChatroom(chatroom) {
chatroom.on('message-received', this.displayNewMessageNotification);
}
disconnectFromChatroom(chatroom) {
chatroom.removeListener('message-received', this.displayNewMessageNotification);
}
}
When the user closes his/her tab or loses their internet connection for a while, naturally, we might want to fire a callback on the server-side that notifies the other users that one of them just went offline. At this point of course it doesn’t make any sense for displayNewMessageNotification
to be invoked for the offline user, but it will continue to be called on new messages unless we remove it explicitly. If we don’t, aside from the unnecessary call, the user object will also stay in memory indefinitely. So be sure to call disconnectFromChatroom
in your server-side callback that executes whenever a user goes offline.
The loose coupling in event-driven architectures can also lead to increased complexity if we’re not careful. It can be difficult to keep track of dependencies in our system i.e. which listeners end up executing on which events. Our application will become especially prone to this problem if we start emitting events from within listeners, possibly triggering chains of unexpected events.
Usama Ashraf
I'm a multilingual, senior software engineer primarily focused on the back-end, databases, infrastructure, containers, DevOps, the cloud. I have considerable front-end experience as well with React and Vue. I'm obsessed with creating scalable, fault-tolerant services with impeccable code quality. I like to share what I know here: https://dev.to/usamaashraf https://medium.com/@usamaashraf And have some humble open-source contributions as well: https://www.npmjs.com/~usama.ashraf https://github.com/UsamaAshraf
See other articles by Usama
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!