A Practical Guide to Reactor Pattern in Node.js

Node.js is considered by many a game-changer—possibly the biggest innovation of the decade in web development. It is loved not just for its technical capabilities, but also for the paradigm shift that it introduced in web development and, in general, in the software development ecosystem.

Some principles and design patterns literally define the developer experience with the Node.js platform and its ecosystem. The most peculiar one is probably its asynchronous nature, which makes heavy use of asynchronous constructs such as callbacks and promises.

In this article, you will gain an understanding of how Node.js works internally and be introduced to the reactor pattern, which is the heart of the asynchronous nature of Node.js. We will go through the main concepts behind the pattern, such as the single-threaded architecture and the non-blocking I/O, and you will see how this creates the foundation for the entire Node.js platform.

I/O is slow

I/O (short for input/output) is definitely the slowest among the fundamental operations of a computer. Accessing the RAM is in the order of nanoseconds (10E-9 seconds) while accessing data on the disk or the network is in the order of milliseconds (10E-3 seconds)

Blocking I/O

In traditional blocking I/O programming, the function call corresponding to an I/O request will block the execution of the thread until the operation completes. This can range from a few milliseconds, in the case of disk access, to minutes or even more, in the case of data being generated from user actions, such as pressing a key. The following pseudocode shows a typical blocking thread performed against a socket:

// blocks the thread until the data is available

data = socket.read()

// data is available

print(data)

Non-blocking I/O

In addition to blocking I/O, most modern operating systems support another mechanism to access resources, called non-blocking I/O. In this operating mode, the system call always returns immediately without waiting for the data to be read or written. If no results are available at the moment of the call, the function will simply return a predefined constant, indicating that there is no data available to return at that moment.

For example, in Unix operating systems, the fcntl() function is used to manipulate an existing file descriptor (which in Unix represents the reference used to access a local file or a network socket) to change its operating mode to non-blocking (with the O_NONBLOCK flag). Once the resource is in non-blocking mode, any read operation will fail with the return code EAGAIN if the resource doesn’t have any data ready to be read.

resources = [socketA, socketB, fileA]

while (!resources.isEmpty()) {

 for (resource of resources) {

 // try to read

 data = resource.read()

 if (data === NO_DATA_AVAILABLE) {

 // there is no data to read at the moment

 continue

 }

 if (data === RESOURCE_CLOSED) {

 // the resource was closed, remove it from the list

 resources.remove(i)

 } else {

 //some data was received, process it

 consumeData(data)

 }

 }

}

Event demultiplexing

Busy-waiting is definitely not an ideal technique for processing non-blocking resources, but luckily, most modern operating systems provide a native mechanism to handle concurrent non-blocking resources in an efficient way. We are talking about the synchronous event demultiplexer (also known as the event notification interface).

If you are unfamiliar with the term, in telecommunications, multiplexing refers to the method by which multiple signals are combined into one so that they can be easily transmitted over a medium with limited capacity.

Demultiplexing refers to the opposite operation, whereby the signal is split again into its original components. Both terms are used in other areas (for example, video processing) to describe the general operation of combining different things into one and vice versa.

The reactor pattern

We can now introduce the reactor pattern, which is a specialization of the algorithms presented in the previous sections. The main idea behind the reactor pattern is to have a handler associated with each I/O operation. A handler in Node.js is represented by a callback (or cb for short) function.

The handler will be invoked as soon as an event is produced and processed by the event loop. The structure of the reactor pattern is shown in the following figure:

Figure: The reactor pattern

This is what happens in an application using the reactor pattern:

  1. The application generates a new I/O operation by submitting a request to the Event Demultiplexer. The application also specifies a handler, which will be invoked when the operation completes. Submitting a new request to the Event Demultiplexer is a non-blocking call and it immediately returns control to the application.
  2. When a set of I/O operations completes, the Event Demultiplexer pushes a set of corresponding events into the Event Queue.
  3. At this point, the Event Loop iterates over the items of the Event Queue.
  4. For each event, the associated handler is invoked.
  5. The handler, which is part of the application code, gives back control to the Event Loop when its execution completes (5a). While the handler executes, it can request new asynchronous operations (5b), causing new items to be added to the Event Demultiplexer (1).
  6. When all the items in the Event Queue are processed, the Event Loop blocks again on the Event Demultiplexer, which then triggers another cycle when a new event is available.

The asynchronous behavior has now become clear. The application expresses interest in accessing a resource at one point in time (without blocking) and provides a handler, which will then be invoked at another point in time when the operation completes.

A Node.js application will exit when there are no more pending operations in the event demultiplexer and no more events to be processed inside the event queue.

We can now define the pattern at the heart of Node.js:

The reactor pattern: Handles I/O by blocking until new events are available from a set of observed resources, and then reacts by dispatching each event to an associated handler.

Summary

In this article, we studied the key concepts behind different patterns and how they created the foundation for the entire Node.js platform. The book Node.js Design Patterns, Third Edition provides a series of best practices and design patterns to help
you create efficient and robust Node.js applications with ease.

Related Articles

Awesome Node.js Tools, Libraries and Resources
Node js Tutorial for Beginners Step by Step With Examples
Node MySQL Connection Pool Example

About the Authors

Mario Casciaro is a software engineer and entrepreneur. Mario worked at IBM for a number of years, first in Rome, then in Dublin Software Lab. He currently splits his time between Var7 Technologies-his own software company-and his role as lead engineer at D4H Technologies where he creates software for emergency response teams.

Luciano Mammino wrote his first line of code at the age of 12 on his father’s old i386. Since then he has never stopped coding. He is currently working at FabFitFun as a principal software engineer where he builds microservices to serve millions of users every day. Luciano also runs bespoke training courses to foster serverless adoption and Fullstack Bulletin, a free weekly newsletter for full-stack developers.

This article is an excerpt from the book Node.js Design Patterns, Third Edition by Mario Casciaro and Luciano Mammino – a comprehensive guide for learning proven patterns, techniques, and tricks to take full advantage of the Node.js platform. It helps you master well-known design principles to create applications that are readable, extensible, and maintainable.

Pankaj Kumar
Pankaj Kumar
Articles: 209