Skip to Content
πŸŽ‰ Welcome to my notes πŸŽ‰
Node.js4. Event Driven Architecture

Event-Driven Architecture in Node.js

Node.js is built on an event-driven architecture that allows it to handle multiple requests concurrently without blocking the main thread. This is a key feature that makes Node.js particularly well-suited for I/O-bound applications, such as web servers and APIs.

πŸ’‘ The Single-Threaded Event Loop

Unlike traditional web servers that create a new thread for every request (which can consume a lot of memory), Node.js operates on a single main thread using an event-driven, non-blocking model.

Think of it like a talented restaurant waiter.

  • Traditional Server (Multi-threaded): Imagine a waiter who takes one customer’s order, goes to the kitchen, and waits until the food is cooked before taking the next order. This is β€œblocking,” and you’d need many waiters to handle many customers.
  • Node.js Server (Event Loop): A Node.js waiter takes a customer’s order (an incoming request), gives it to the kitchen (Node’s background C++ APIs), and immediately moves on to the next customer. When the kitchen finishes a dish, it puts it on the counter (the event queue), and the waiter picks it up and delivers it when they have a free moment.

This single waiter (the event loop) can handle hundreds of customers concurrently because they never wait. This is the essence of non-blocking I/O.

βš™οΈ The Low-Level Components

The event-driven architecture relies on a few key components working together.

  1. The Call Stack: This is where your JavaScript code is executed. It’s a β€œfirst-in, last-out” stack. When you call a function, it’s added to the top of the stack; when it returns, it’s popped off.
  2. Node APIs & libuv: When your code calls an asynchronous function (e.g., for file reading or a database query), Node.js doesn’t execute it in your main thread. It hands the task off to its powerful C++ APIs, primarily a library called libuv. libuv manages a thread pool to handle these heavy operations in the background without blocking your JavaScript code.
  3. The Callback Queue (Event Queue): When an asynchronous task managed by libuv is complete, its associated callback function is placed in the callback queue. It waits here patiently for its turn to be executed.
  4. The Event Loop: This is the heart of the process. The event loop’s job is simple but crucial: it constantly checks if the call stack is empty. If it is, the event loop takes the first event (callback) from the queue and pushes it onto the call stack, which then executes it.

πŸ§‘β€πŸ’» The Traditional Way

Let’s trace the execution of a simple file read.

event-loop-example.js
import fs from "fs"; console.log("1. Program Started"); fs.readFile("my-file.txt", "utf8", (err, data) => { // This is the callback function if (err) throw err; console.log("3. File content read successfully!"); }); console.log("2. This will print before the file is read.");

πŸ“’ The High-Level Pattern: EventEmitter

While the event loop manages low-level system events, Node.js provides a user-facing pattern for working with events in your own code. This is known as the Observer or Publish-Subscribe pattern, and it’s built around the EventEmitter class.

  • EventEmitter (The Publisher πŸ“»): This is a class from Node’s built-in events module. An object of this class, the β€œemitter,” can produce or β€œemit” named events. Many core Node.js objects, like HTTP servers and file streams, inherit from EventEmitter. You use its .emit('eventName', ...args) method to broadcast an event.

  • EventListener / EventHandler (The Subscriber 🎧): This is the function that β€œlistens” for an event. You register this function using the emitter’s .on('eventName', eventHandler) method. When the EventEmitter broadcasts a specific event, all registered listeners for that event name are executed.

πŸ“’ EventEmitter in Action: The PizzaShop

This example shows how to create a custom class that inherits from EventEmitter, register listeners for an event, and then emit that event with data.

event-emitter-example.js
// 1. Import the EventEmitter class from the 'events' module. const EventEmitter = require("events"); // 2. Create a custom class that extends EventEmitter. // This is our "Publisher" or "Emitter". class PizzaShop extends EventEmitter { constructor() { super(); // Call the parent class constructor. this.orderNumber = 0; } // A method to place an order, which will emit an event. order(size, topping) { this.orderNumber++; console.log( `Order #${this.orderNumber} placed for a ${size} pizza with ${topping}.` ); // 3. Emit the 'order' event and pass the data along. this.emit("order", this.orderNumber, size, topping); } } // 4. Create an instance of our EventEmitter. const myPizzaShop = new PizzaShop(); // 5. Define our "EventListener" / "EventHandler" functions. // These are the "Subscribers". const kitchenHandler = (orderNum, size, topping) => { console.log( `[Kitchen] Preparing order #${orderNum}: A ${size} pizza with ${topping}.` ); }; const notificationHandler = (orderNum) => { console.log(`[Notification] Sent SMS for order #${orderNum}.`); }; // 6. Register the listeners for the 'order' event. myPizzaShop.on("order", kitchenHandler); myPizzaShop.on("order", notificationHandler); // 7. Trigger the event by calling the order method. // This will cause all registered listeners to execute. myPizzaShop.order("large", "mushrooms"); myPizzaShop.order("small", "pepperoni");

πŸ› οΈ How It Works in Practice

Execution Flow:

  1. A synchronous operation is pushed to the call stack and executed immediately.
  2. An asynchronous function is pushed to the call stack. Node.js sees it’s an async operation and hands the task to libuv. The function call is then popped from the stack.
  3. The main thread is not blocked. It moves on to the next line of synchronous code, which is pushed to the call stack and executed immediately. The call stack is now empty.
  4. Sometime later, libuv finishes its task. It takes the associated callback function and places it in the callback queue.
  5. The event loop sees that the call stack is empty and there’s an item in the queue. It pushes the callback onto the stack.
  6. The callback function is executed last.
Last updated on