NodeJS Interview Questions & Answers (2026)
Top NodeJS interview questions and answers for freshers, experienced developers and senior engineers.
Junior Question: What is NodeJS?
Level: Junior | Category: Fundamentals | Time: 7 mins
Expected Answer
NodeJS is a JavaScript runtime built on Google's V8 Engine that allows JavaScript
to run outside the browser.
Key Characteristics
- Single-threaded
- Event-driven
- Non-blocking I/O
- Asynchronous programming model
Common Use Cases
- REST APIs
- Real-time applications
- Microservices
- Streaming services
- Chat applications
Interviewer Note: 💡 Candidate should clearly explain that NodeJS is a runtime environment,
not a framework.
Mid-Level Question: Explain the Event Loop in NodeJS
Level: Mid-Level | Category: Asynchronous & Event Loop | Time: 10 mins
Expected Answer
The Event Loop allows NodeJS to perform non-blocking operations even though
JavaScript runs on a single thread.
Execution Flow
- Call Stack
- Node APIs / Libuv
- Callback Queue
- Event Loop
Example
Code Example
console.log('Start');
setTimeout(() => {
console.log('Timer');
}, 0);
console.log('End');
Interviewer Note: 💡 Follow-Up Question:
Why does Timer execute after End?
The callback enters the Callback Queue and waits until the Call Stack becomes empty.
Mid-Level Practical: Predict the Output
Level: Mid-Level | Category: Asynchronous & Event Loop | Time: 10 mins
Code Example
console.log('A');
setTimeout(() => console.log('B'));
Promise.resolve()
.then(() => console.log('C'));
console.log('D');
Expected Answer
Correct Output
Expected Answer
Explanation
- Promise callbacks enter Microtask Queue
- Microtasks execute before Callback Queue
- setTimeout callback executes last
Senior Question: Streams vs Buffers
Level: Senior | Category: Streams & Threads | Time: 12 mins
Expected Answer
Both are used for handling data, but work differently.
Streams
fs.createReadStream('large-file.txt');
- Processes data in chunks
- Low memory usage
- Ideal for large files
Buffers
fs.readFile('large-file.txt');
- Loads entire file into memory
- Simpler API
- Not ideal for huge files
Interviewer Note: 💡 Senior Candidate
Candidate should discuss memory consumption,
streaming performance and backpressure handling.
Senior Question: What are Worker Threads?
Level: Senior | Category: Streams & Threads | Time: 12 mins
Expected Answer
Worker Threads allow CPU-intensive tasks to run on separate threads,
preventing the Event Loop from being blocked.
Code Example
const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js');
Expected Answer
Common Use Cases
- Image processing
- PDF generation
- Data analysis
- Encryption
Interviewer Note: 💡 When should Worker Threads NOT be used?
For I/O operations because NodeJS already handles them asynchronously.
Practical Coding Round: Implement a Simple Rate Limiter
Level: Coding Challenge | Category: Streams & Threads | Time: 15 mins
Expected Answer
Restrict a user to a fixed number of requests.
Code Example
const requests = {};
function allowRequest(ip) {
requests[ip] ??= 0;
if (requests[ip] >= 5) {
return false;
}
requests[ip]++;
return true;
}
Expected Answer
Interview Discussion Points
- Memory usage
- Distributed systems
- Redis implementation
- Sliding window algorithm
Architect Level Question: How Would You Scale NodeJS to 10 Million Users?
Level: Architect | Category: Scaling & Architecture | Time: 15 mins
Expected Answer
Expected Topics
- Load Balancer
- Horizontal Scaling
- Microservices
- Redis Cache
- Database Sharding
- CDN
- Message Queues
- Monitoring
Interviewer Note: 💡 Red Flag:
Candidate only talks about increasing server RAM.
Strong Candidate:
Discusses architecture, bottlenecks,
caching strategy and distributed systems.
Junior Question #8: What is package.json and what is the difference between dependencies and devDependencies?
Level: Junior | Category: npm & Configuration | Time: 6 mins
Expected Answer
The package.json file is the manifest of a Node.js project. It holds metadata (name, version, main file) and manages project dependencies.
- dependencies: Essential packages required to run the application in production (e.g., Express, Lodash).
- devDependencies: Packages only needed during local development, testing, or building (e.g., Jest, ESLint, TypeScript compiler).
Code Example
{
"name": "my-app",
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"jest": "^29.5.0"
}
}
Interviewer Note: 💡 Candidate should mention that npm install --production will skip devDependencies to save space in container deployments.
Junior Question #9: Explain the difference between exports and module.exports in CommonJS Modules.
Level: Junior | Category: Modules (CommonJS & ESM) | Time: 7 mins
Expected Answer
In CommonJS, module.exports is the actual object returned when requiring a module. exports is merely a shorthand variable pointing to the same memory reference as module.exports.
If you assign a new object or function to exports directly (e.g., exports = MyClass), you break the reference, and Node.js will still return the original module.exports (which remains empty).
Code Example
// Correct exports shorthand
exports.add = (a, b) => a + b;
// Correct default override
module.exports = (a, b) => a + b;
Interviewer Note: 💡 Ask: What happens if I write exports = { hello: 'world' }? Candidate should explain that this breaks the reference.
Junior Question #10: What is the purpose of package-lock.json and why should it be committed to version control?
Level: Junior | Category: npm & Configuration | Time: 6 mins
Expected Answer
The package-lock.json file locks down the exact version of all dependencies (and their nested dependencies) installed in the project. It records the precise dependency tree with hash checksums to ensure reproducible builds across machines.
It must be committed to git to prevent drift between local development, staging, and production environments.
Interviewer Note: 💡 Red Flag: Candidate suggests deleting package-lock.json to resolve merge conflicts.
Junior Question #11: How do you pass environment variables to a Node.js process and access them?
Level: Junior | Category: Process & Configuration | Time: 5 mins
Expected Answer
Environment variables are passed from the command line prefixing the execution script and are accessed inside the runtime via the global object process.env.
Code Example
// Command line execution:
// PORT=4000 node index.js
const port = process.env.PORT || 3000;
console.log(`Server listening on port ${port}`);
Interviewer Note: 💡 Ask about dotenv package usage for local development profiles.
Junior Question #12: What is the difference between setImmediate() and process.nextTick()?
Level: Junior | Category: Event Loop | Time: 8 mins
Expected Answer
Although they sound opposite:
process.nextTick() executes callbacks immediately after the current phase of the event loop completes, before the loop enters the next phase. It runs microtasks.
setImmediate() schedules execution on the next turn (tick) of the event loop during the check phase.
Code Example
process.nextTick(() => console.log('Tick'));
setImmediate(() => console.log('Immediate'));
// Output:
// Tick
// Immediate
Interviewer Note: 💡 High-quality candidates warn that recursive process.nextTick calls can starve I/O execution.
Junior Question #13: How do you handle file path operations cross-platform in Node.js?
Level: Junior | Category: File System & Path | Time: 6 mins
Expected Answer
Instead of manual string concats using slashes, use the built-in path module methods like path.join() and path.resolve(). They automatically format paths with delimiters matching the runtime OS (Windows \ vs Unix /).
Code Example
const path = require('path');
// Safe cross-platform absolute path
const uploadDir = path.join(__dirname, 'uploads', 'images');
console.log(uploadDir);
Interviewer Note: 💡 Mention the difference between __dirname (current directory of the file) and process.cwd() (current working directory of Node process).
Junior Question #14: What is the EventEmitter class and how do you use it?
Level: Junior | Category: Events & Callbacks | Time: 7 mins
Expected Answer
The EventEmitter is a core class of the events module. It implements the Observer pattern, allowing one part of the code to emit named event markers that trigger registered listener callback functions.
Code Example
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
myEmitter.on('login', (user) => {
console.log(`User logged in: ${user}`);
});
myEmitter.emit('login', 'Vijay');
Interviewer Note: 💡 Emphasize avoiding memory leaks by removing listeners via off() or removeListener().
Junior Question #15: What is the difference between fs.readFile and fs.readFileSync?
Level: Junior | Category: File System & Path | Time: 6 mins
Expected Answer
fs.readFile is asynchronous and non-blocking, delegating the reading task to the Libuv thread pool. fs.readFileSync is synchronous and blocking, stalling the main event loop thread until the entire file is fetched.
Interviewer Note: 💡 Highlight: readFileSync should only be used during application bootstrap configurations.
Junior Question #16: How do you create a basic HTTP server without frameworks?
Level: Junior | Category: HTTP & API Design | Time: 6 mins
Expected Answer
Use the built-in http module and call the createServer() factory method.
Code Example
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello NodeJS World');
});
server.listen(3000);
Interviewer Note: 💡 Junior candidates should understand req (IncomingMessage) and res (ServerResponse) params.
Junior Question #17: What is Express.js middleware?
Level: Junior | Category: Express.js | Time: 7 mins
Expected Answer
Middleware functions are functions that have access to the request object (req), response object (res), and the next middleware function in the application’s request-response cycle.
They can execute code, modify request/response objects, end request cycles, or call next() to pass control to subsequent handlers.
Code Example
const logger = (req, res, next) => {
console.log(`${req.method} request received`);
next(); // Mandatory to pass control
};
Interviewer Note: 💡 Red Flag: Forgetting to call next() resulting in hung requests.
Junior Question #18: What is the global scope in Node.js and how does it differ from browsers?
Level: Junior | Category: Node.js Fundamentals | Time: 5 mins
Expected Answer
In browsers, the top-level global scope is window. In Node.js, the global scope is global. Also, variables declared with var at the top level of a Node.js file are local to that module wrapper, not added to the global scope like browsers.
Interviewer Note: 💡 Good to mention globalThis keyword in modern ES2020+ standards.
Junior Question #19: What is the difference between npm and npx?
Level: Junior | Category: npm & Configuration | Time: 5 mins
Expected Answer
npm is the package manager used to install, update, and manage dependencies in a project. npx is a package runner tool shipped with npm. It allows executing CLI packages (e.g. create-react-app, prisma) directly from the registry without installing them globally.
Interviewer Note: 💡 Mention that npx downloads and executes the latest package in a temporary directory.
Junior Question #20: What are buffers in Node.js and why are they needed?
Level: Junior | Category: Streams & Buffers | Time: 6 mins
Expected Answer
A Buffer represents a fixed-size chunk of memory allocated outside the V8 heap. It is designed to handle raw binary data streams (such as files, TCP packets, images) which JavaScript originally had no native type for.
Code Example
const buf = Buffer.from('Hello');
console.log(buf); // Output raw bytes:
console.log(buf.toString()); // Output: Hello
Interviewer Note: 💡 Juniors should know Buffer.alloc(size) allocates pre-cleared raw memory.
Junior Question #21: What is type coercion and how does Node.js developers avoid it?
Level: Junior | Category: Node.js Fundamentals | Time: 5 mins
Expected Answer
Type coercion is the automatic conversion of values from one data type to another (e.g., 5 + '5' = '55'). Node.js developers avoid coercion by using strict equality comparisons (=== and !==) instead of loose ones (== and !=).
Interviewer Note: 💡 Standard question, verify basic understanding of truthy/falsy.
Junior Question #22: Explain how routing works in Express.js
Level: Junior | Category: Express.js | Time: 6 mins
Expected Answer
Routing determines how an application responds to a client request to a particular endpoint (URI/path) and HTTP request method (GET, POST, etc.). Express supports route parameters and router groups.
Code Example
const express = require('express');
const app = express();
app.get('/users/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
Interviewer Note: 💡 Ask difference between route parameters (req.params) and query parameters (req.query).
Junior Question #23: How do you parse request body payloads in Express.js?
Level: Junior | Category: Express.js | Time: 6 mins
Expected Answer
Use built-in body parsing middlewares. Since Express 4.16+, body-parser is bundled within Express directly, enabling express.json() for JSON payloads and express.urlencoded() for form-submitted key-values.
Code Example
const express = require('express');
const app = express();
app.use(express.json()); // Parses JSON bodies
app.post('/data', (req, res) => {
console.log(req.body); // holds JSON parsed content
res.sendStatus(200);
});
Interviewer Note: 💡 Red Flag: Failing to register body parsing middleware and wondering why req.body is undefined.
Junior Question #24: What is the purpose of npm shrinkwrap?
Level: Junior | Category: npm & Configuration | Time: 6 mins
Expected Answer
npm shrinkwrap creates a locked manifest file (npm-shrinkwrap.json) that behaves exactly like package-lock.json, but is publishable to the public npm registry to enforce lock versions on consumer installs.
Interviewer Note: 💡 Standard locking explanation is sufficient.
Junior Question #25: What are core modules in Node.js and name a few?
Level: Junior | Category: Node.js Fundamentals | Time: 5 mins
Expected Answer
Core modules are modules pre-compiled into the Node.js binary. They don't require npm installs and are imported directly via require/import statements.
Examples: fs, path, http, os, crypto, events, util.
Interviewer Note: 💡 Junior check: Can they enumerate basic built-ins?
Junior Question #26: What is REPL in Node.js?
Level: Junior | Category: Node.js Fundamentals | Time: 4 mins
Expected Answer
REPL stands for **Read-Eval-Print-Loop**. It is the interactive shell interface launched when running the node command alone, allowing immediate execution of JS commands for testing.
Interviewer Note: 💡 Standard definition.
Junior Question #27: Explain how query strings are read in Express.js
Level: Junior | Category: Express.js | Time: 5 mins
Expected Answer
Query parameters following the ? symbol in an API url are parsed by Express automatically and populated inside the req.query object.
Code Example
// GET /search?q=nodejs&limit=10
app.get('/search', (req, res) => {
const query = req.query.q;
const limit = req.query.limit;
res.send(`Search for ${query}, limit ${limit}`);
});
Interviewer Note: 💡 Make sure they understand difference between params and query.
Mid-Level Question #28: Explain backpressure in Node.js streams and how to handle it.
Level: Mid-Level | Category: Streams & Buffers | Time: 10 mins
Expected Answer
Backpressure occurs when the data writing buffer speed is slower than the data reading buffer speed in streams (e.g. fast disk read piped to a slow HTTP client). This leads to memory accumulation and eventually OOM crashes.
To handle it, stream consumers notify publishers to pause reading by returning false on write(). The stream then fires the drain event once the buffer clears, indicating it is safe to resume reading.
Code Example
const fs = require('fs');
const readStream = fs.createReadStream('large.txt');
const writeStream = fs.createWriteStream('copy.txt');
readStream.on('data', (chunk) => {
const canContinue = writeStream.write(chunk);
if (!canContinue) {
readStream.pause(); // Pause reading to handle backpressure
}
});
writeStream.on('drain', () => {
readStream.resume(); // Resume reading once buffer is flushed
});
Interviewer Note: 💡 Candidate should mention that .pipe() handles backpressure out of the box.
Mid-Level Question #29: What is child_process.fork() and how does IPC (Inter-Process Communication) work?
Level: Mid-Level | Category: Performance & Concurrency | Time: 9 mins
Expected Answer
child_process.fork() is a special helper of spawn to create a new Node.js child process executing a specified JS module. Unlike standard processes, fork creates an IPC channel allowing bidirectional message passage between the parent and child via send() and on('message').
Code Example
// parent.js
const { fork } = require('child_process');
const child = fork('child.js');
child.send({ hello: 'child' });
child.on('message', (msg) => console.log('Parent received:', msg));
// child.js
process.on('message', (msg) => {
console.log('Child received:', msg);
process.send({ hello: 'parent' });
});
Interviewer Note: 💡 Ask: How does fork compare to spawn? Fork runs a new Node.js engine, whereas spawn executes any system CLI binary.
Mid-Level Question #30: Compare spawn(), exec(), execFile(), and fork() methods in child_process.
Level: Mid-Level | Category: Performance & Concurrency | Time: 10 mins
Expected Answer
These helpers spawn child processes with specific configurations:
- spawn(): Stream-based execution of CLI commands. Best for returning large amounts of data dynamically without blocking memory.
- exec(): Spawns a shell and buffers the output (default limit: 200KB). Best for running small scripts and receiving immediate output.
- execFile(): Runs a file binary directly without spawning a shell, saving memory and improving execution speed.
- fork(): Spawns a new Node.js process and creates an IPC communications channel.
Interviewer Note: 💡 Red Flag: Spawning exec() on user-inputted strings, introducing shell injection vulnerabilities.
Mid-Level Question #31: What is libuv, what is its default thread pool size, and how do you change it?
Level: Mid-Level | Category: Event Loop | Time: 9 mins
Expected Answer
libuv is the multi-platform C library supporting asynchronous I/O operations in Node.js. It manages the Event Loop and a Worker Thread Pool for task delegations that are not natively asynchronous at the OS level (like file system access, crypto hashing, DNS resolution).
The default size is 4 threads. It can be modified on startup by setting the environment variable UV_THREADPOOL_SIZE (up to a limit of 1024).
Interviewer Note: 💡 Ask: Which core modules utilize the thread pool? (fs, crypto, zlib, dns.lookup).
Mid-Level Question #32: What is JWT (JSON Web Tokens) and how do you implement token authentication in Node.js?
Level: Mid-Level | Category: Security | Time: 10 mins
Expected Answer
JWT is a stateless credential format consisting of three base64url strings: Header, Payload, and Signature. It is passed via authorization request headers to authenticate users without storing session states in database tables.
Code Example
const jwt = require('jsonwebtoken');
// Generate Token
const token = jwt.sign({ userId: 123 }, 'secret_key', { expiresIn: '1h' });
// Verify Token Middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).send('Access Denied');
try {
const decoded = jwt.verify(token, 'secret_key');
req.user = decoded;
next();
} catch (err) {
res.status(403).send('Invalid Token');
}
};
Interviewer Note: 💡 JWTs are stateless; revoking them prior to expiration requires blacklist checks or db tokens validation.
Mid-Level Question #33: How does session-based authentication differ from token-based authentication?
Level: Mid-Level | Category: Security | Time: 9 mins
Expected Answer
Differences are centered around state management:
- Session-based: Stateful. The server creates a session entry in a database/memory cache (Redis) and returns a unique session ID stored in client cookies. Subsequent queries validate the session ID against the server session repository.
- Token-based: Stateless. The server issues a signed JWT containing client data. The client stores it locally (LocalStorage or HttpOnly Cookies) and attaches it to request headers. The server verifies signature validity without querying cache databases.
Interviewer Note: 💡 Senior candidate should point out that JWTs cannot easily be invalidated on-demand.
Mid-Level Question #34: How do you hash passwords securely in Node.js?
Level: Mid-Level | Category: Security | Time: 8 mins
Expected Answer
Do not use simple hash algorithms like MD5 or SHA256 alone because they are vulnerable to dictionary and rainbow table lookups. Instead, use slow, adaptive hashing libraries like bcrypt or Node.js native crypto.scrypt(), applying unique salt strings.
Code Example
const bcrypt = require('bcrypt');
// Hash password
const hashPassword = async (pwd) => {
const salt = await bcrypt.genSalt(10);
return await bcrypt.hash(pwd, salt);
};
// Compare password
const verifyPassword = async (pwd, hash) => {
return await bcrypt.compare(pwd, hash);
};
Interviewer Note: 💡 Check: Does the candidate understand why salting passwords is required?
Mid-Level Question #35: How do you implement rate limiting in an Express application?
Level: Mid-Level | Category: Express.js | Time: 8 mins
Expected Answer
Rate limiting restricts the number of requests a client can make in a specified window of time, preventing DDoS and brute-force scenarios. Locally, you can use packages like express-rate-limit, but distributed architectures require storing hits in a central Redis cache database.
Code Example
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per IP
message: 'Too many requests, please try again later.'
});
app.use('/api/', apiLimiter);
Interviewer Note: 💡 Ask: What headers does a rate limiter return? (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After).
Mid-Level Question #36: What is Helmet middleware and how does it secure Express headers?
Level: Mid-Level | Category: Security | Time: 7 mins
Expected Answer
helmet is an Express middleware collection that sets various secure HTTP response headers to defend against clickjacking, sniff attacks, XSS, and server details identification.
It hides headers like X-Powered-By: Express and enforces security constraints like Strict-Transport-Security, X-Content-Type-Options: nosniff, and Content-Security-Policy.
Interviewer Note: 💡 Ask: Why should we hide the X-Powered-By header? (To prevent attackers from profiling our framework versions).
Mid-Level Question #37: Compare WebStreams and Node.js standard Streams.
Level: Mid-Level | Category: Streams & Buffers | Time: 9 mins
Expected Answer
Node.js standard streams (e.g. Readable, Writable) are proprietary to the Node.js runtime and emit events like data, end, and error. WebStreams are standard web API stream specifications (e.g. ReadableStream, WritableStream) supported inside browsers, Cloudflare Workers, and modern Node.js versions, allowing isomorphic streams logic.
Interviewer Note: 💡 Modern node APIs (like fetch) return WebStreams by default.
Mid-Level Question #38: What is AbortController and how is it used in Node.js?
Level: Mid-Level | Category: Async Programming | Time: 8 mins
Expected Answer
AbortController is a standard controller object that communicates termination requests to async tasks (like fetch requests, file reads, or timeouts) using an AbortSignal.
Code Example
const controller = new AbortController();
const { signal } = controller;
// Cancel fetch query after 500ms
setTimeout(() => controller.abort(), 500);
try {
const res = await fetch('https://api.example.com/data', { signal });
const data = await res.json();
} catch (err) {
if (err.name === 'AbortError') console.log('Request cancelled!');
}
Interviewer Note: 💡 Mention that AbortController was standardized in Node.js 15+.
Mid-Level Question #39: How do you test Node.js controllers using Jest and Supertest?
Level: Mid-Level | Category: Testing | Time: 10 mins
Expected Answer
Supertest allows testing HTTP server endpoints without binding the Express application to network ports. Jest manages the test framework assertions, setup/teardown processes, and execution hooks.
Code Example
const request = require('supertest');
const app = require('./app'); // Express app object
describe('GET /api/users', () => {
it('should return list of users', async () => {
const res = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).toBeInstanceOf(Array);
});
});
Interviewer Note: 💡 Emphasize not calling app.listen() inside app.js directly, export app and call listen inside server.js to allow testing isolation.
Mid-Level Question #40: Explain Node.js 20+ built-in Test Runner API.
Level: Mid-Level | Category: Testing | Time: 8 mins
Expected Answer
Node.js 20 introduces a stable built-in test runner via the node:test module, removing external test dependencies (like Jest or Mocha) for lightweight test scripts.
Code Example
const test = require('node:test');
const assert = require('node:assert');
test('synchronous addition test', (t) => {
assert.strictEqual(1 + 1, 2);
});
Interviewer Note: 💡 Ask about test runner coverage and execution speed benefits.
Mid-Level Question #41: What is the difference between process.exit(0) and process.exit(1)?
Level: Mid-Level | Category: Process & Configuration | Time: 5 mins
Expected Answer
Both exit code numbers tell the parent OS process status when Node exits:
- 0: Success/Normal termination. The script executed completely with no runtime errors.
- 1: Failure/Abnormal exit. Tells orchestrators (like Kubernetes, PM2, or Docker) that an error occurred, prompting restart configurations.
Interviewer Note: 💡 Good candidate notes that process.exit should rarely be called inside API controllers.
Mid-Level Question #42: How does Node.js resolve modules under node_modules (Module Resolution Algorithm)?
Level: Mid-Level | Category: Modules (CommonJS & ESM) | Time: 9 mins
Expected Answer
Node.js starts resolving from the current file folder looking for a node_modules folder. If not found, it climbs up parent directory folders iteratively to the root directory. Inside the module folder, it reads the package.json exports, main entry, or defaults to index.js.
Interviewer Note: 💡 Ask: How does node behave when a file import lacks extension? It resolves .js, .json, and .node in order.
Mid-Level Question #43: How does the util.promisify method work?
Level: Mid-Level | Category: Async Programming | Time: 7 mins
Expected Answer
util.promisify transforms standard node callback-based functions (expecting error-first callback params) into clean Promise-returning utilities compatible with async/await patterns.
Code Example
const { promisify } = require('util');
const fs = require('fs');
const readFileAsync = promisify(fs.readFile);
try {
const content = await readFileAsync('file.txt', 'utf8');
console.log(content);
} catch (err) {
console.error(err);
}
Interviewer Note: 💡 Ask: What are callback signature standards for promisify? (Error first, data second).
Mid-Level Question #44: What is the purpose of process.on('uncaughtException')?
Level: Mid-Level | Category: Process & Configuration | Time: 8 mins
Expected Answer
It listens to unhandled synchronous errors that bubble up to the global execution scope. This prevents standard crashes from dropping active requests, although it is best practice to log the error and terminate the process gracefully to avoid memory corruption states.
Code Example
process.on('uncaughtException', (err) => {
console.error('Fatal Uncaught Exception:', err);
// Gracefully terminate application after logging
process.exit(1);
});
Interviewer Note: 💡 Red Flag: Listening to uncaughtException, logging it, and keeping the process running, which leads to socket leaks.
Mid-Level Question #45: What is process.on('unhandledRejection')?
Level: Mid-Level | Category: Process & Configuration | Time: 7 mins
Expected Answer
It captures promise rejections that lack corresponding catch() handlers. Starting in modern Node.js versions, unhandled rejections will exit the process with status code 1 if not handled.
Interviewer Note: 💡 Emphasize registering catch blocks on all promise pipelines.
Mid-Level Question #46: How do you connect WebSockets using the ws package in Node.js?
Level: Mid-Level | Category: Express.js | Time: 8 mins
Expected Answer
WebSockets enable persistent, full-duplex TCP communication channels over port connections. In Node.js, the ws package sets up lightweight WebSocket servers.
Code Example
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
console.log(`Received: ${message}`);
ws.send('Acknowledged');
});
});
Interviewer Note: 💡 Contrast WebSocket with standard polling or SSE (Server-Sent Events).
Mid-Level Question #47: What is Joi / Zod and why is request validation essential?
Level: Mid-Level | Category: Express.js | Time: 8 mins
Expected Answer
Validation libraries like Zod or Joi enforce strict type schemas on client input (req.body, req.query, req.params) before route controllers run, preventing SQL injection, schema corruption, or invalid arguments from crashing the app.
Code Example
const { z } = require('zod');
const userSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
app.post('/register', (req, res) => {
const result = userSchema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error);
res.send('Valid payload');
});
Interviewer Note: 💡 Zod supports TypeScript type inference out of the box.
Mid-Level Question #48: What is the difference between path.join() and path.resolve()?
Level: Mid-Level | Category: File System & Path | Time: 7 mins
Expected Answer
Path processing differences:
- path.join(): Joins all path segments together using OS path delimiters, returning a normalized relative path string.
- path.resolve(): Resolves path segments into an absolute path. It processes inputs from right to left, prefixing the current working directory (
cwd) if a absolute path is not resolved.
Interviewer Note: 💡 Ask: If path.resolve('/a', 'b') runs, what is the output? (/a/b absolute path).
Mid-Level Question #49: How do you run shell commands using child_process.exec()?
Level: Mid-Level | Category: Performance & Concurrency | Time: 7 mins
Expected Answer
exec runs command line inputs inside a system shell and passes the buffered outputs to a callback function.
Code Example
const { exec } = require('child_process');
exec('ls -lh', (error, stdout, stderr) => {
if (error) return console.error(`Error: ${error.message}`);
console.log(`Output: ${stdout}`);
});
Interviewer Note: 💡 Red flag check: exec holds the output in memory buffer (default 200KB). Large output will crash the application.
Mid-Level Question #50: What are the common HTTP response codes used in Node.js REST APIs?
Level: Mid-Level | Category: HTTP & API Design | Time: 6 mins
Expected Answer
Standard REST codes include:
- 200: OK (Success)
- 201: Created (Success write)
- 400: Bad Request (Validation failed)
- 401: Unauthorized (No auth credentials)
- 403: Forbidden (Insufficient permission credentials)
- 404: Not Found
- 500: Internal Server Error
Interviewer Note: 💡 Ask difference between 401 and 403 (unauthenticated vs unauthorized).
Senior Question #51: Explain V8 memory spaces (New, Old, Large Objects) and how Garbage Collection works.
Level: Senior | Category: Memory & Concurrency | Time: 12 mins
Expected Answer
V8 divides heap memory into spaces for optimized GC cycles:
- New Space (Nursery): Small space (1-8MB) where new allocations occur. It uses a fast **Scavenge (Copy)** collector. Surviving objects are promoted to Old Space.
- Old Space: Holds long-lived objects. Divided into Old Pointer Space and Old Data Space. It uses **Mark-Sweep-Compact** algorithms.
- Large Object Space: Holds objects exceeding size allocations of other spaces, avoiding copy overhead.
V8 GC runs incrementally and concurrently to minimize pause times on the main event loop thread.
Interviewer Note: 💡 Ask: How does node flag GC cycles? (Use node --expose-gc flag to trigger gc programmatically).
Senior Question #52: How do you detect, profile, and debug memory leaks in Node.js?
Level: Senior | Category: Memory & Concurrency | Time: 12 mins
Expected Answer
Memory leak detection and debugging flow:
- Detection: Monitor memory usage (RSS/heapUsed) growing consistently over time in APMs (Datadog, Grafana) or using
process.memoryUsage().
- Snapshot Generation: Run node with
--inspect, connect Chrome DevTools, and take Heap Snapshots at different intervals (before and after load tests).
- Comparison: Compare snapshots to find growing allocations (often unreleased event listeners, global arrays, or closures).
Code Example
// Programmatic snapshot generation
const v8 = require('v8');
const fs = require('fs');
const snapshotStream = v8.getHeapSnapshot();
const fileName = `${Date.now()}.heapsnapshot`;
const fileStream = fs.createWriteStream(fileName);
snapshotStream.pipe(fileStream);
Interviewer Note: 💡 Seniors should reference tools like clinic.js (clinic doctor / clinic heapprofiler).
Senior Question #53: How do you perform CPU profiling on a Node.js process?
Level: Senior | Category: Performance & Diagnostics | Time: 11 mins
Expected Answer
Use the built-in V8 profiler to diagnose event loop delays:
- Run Node with the built-in tick profiler:
node --prof app.js.
- Run load tests to simulate CPU pressure.
- Process the generated log file:
node --prof-process isolate-0xXXXX-v8.log > processed.txt.
- Analyze the output to locate hot paths (blocking regex, crypto, parsing).
Interviewer Note: 💡 Good to mention flamegraph tools to visualize call stack frequencies.
Senior Question #54: Compare Worker Threads and Child Processes in Node.js.
Level: Senior | Category: Performance & Concurrency | Time: 12 mins
Expected Answer
Differences are centered around memory sharing models:
- Child Processes: Each process runs on its own OS process, possessing its own V8 instance and isolated memory. Communication occurs through serialized IPC messaging. Best for external shell executions or long running tasks.
- Worker Threads: Run inside the same OS process on separate threads. They share memory through
SharedArrayBuffer and avoid expensive serialization overheads. Best for CPU-intensive JavaScript operations.
Code Example
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (msg) => console.log('Received:', msg));
} else {
parentPort.postMessage('Done');
}
Interviewer Note: 💡 Worker Threads do not make Node.js multi-threaded for standard I/O; they target CPU loads.
Senior Question #55: How do you implement Redis distributed caching in Node.js?
Level: Senior | Category: Scaling & Infrastructure | Time: 12 mins
Expected Answer
Implement a Cache-Aside pattern. Look up key data in Redis cache; on miss, query database, save results in Redis with a TTL (Time To Live), and return payload.
Code Example
const redis = require('redis');
const client = redis.createClient({ url: 'redis://localhost:6379' });
const getUserCached = async (userId) => {
const cached = await client.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
const user = await db.queryUser(userId);
await client.setEx(`user:${userId}`, 3600, JSON.stringify(user)); // 1h TTL
return user;
};
Interviewer Note: 💡 Ask about cache stampede (dogpiling) and how to handle lock synchronizations.
Senior Question #56: Explain the Circuit Breaker pattern and how to implement it in Node.js.
Level: Senior | Category: Scaling & Infrastructure | Time: 12 mins
Expected Answer
A Circuit Breaker wraps third-party network calls. If failures exceed a threshold (e.g. 50% errors), the breaker trips (opens) and immediately returns error responses instead of routing queries that pile up sockets on hanging downstreams. It moves to a **Half-Open** probe state after a cooldown to test if services recovered.
Code Example
const opossum = require('opossum');
const asyncFunction = async (id) => {
const res = await fetch(`https://partner-api/users/${id}`);
return res.json();
};
const options = {
timeout: 3000, // 3s timeout
errorThresholdPercentage: 50, // open if 50% fail
resetTimeout: 30000 // try reset after 30s
};
const breaker = new opossum(asyncFunction, options);
breaker.fallback(() => ({ error: 'Service Unavailable' }));
const data = await breaker.fire(123);
Interviewer Note: 💡 Essential for microservice resiliency design questions.
Senior Question #57: How do you handle database connection pooling in high-concurrency Node.js apps?
Level: Senior | Category: HTTP & API Design | Time: 10 mins
Expected Answer
Opening a TCP connection for every query introduces latency and CPU spikes. Connection pooling maintains a pre-allocated pool of active database connections. Node.js processes lease active connections from the pool, execute SQL queries, and immediately return them to the pool.
Interviewer Note: 💡 Red Flag: Opening new client connections in request handlers and forgetting to call end().
Senior Question #58: Explain Event Sourcing pattern and how to design it.
Level: Senior | Category: Design Patterns | Time: 11 mins
Expected Answer
Instead of updating database entity row values in place, Event Sourcing records every state modification as an immutable, sequential event stream (e.g. OrderCreated, ItemAdded, OrderPaid). The current state of an entity is derived by replaying (reducing) the events chronologically.
Interviewer Note: 💡 Candidate should discuss CQRS (Command Query Responsibility Segregation) integration.
Senior Question #59: Explain the Node.js Permission Model introduced in modern releases.
Level: Senior | Category: Security | Time: 10 mins
Expected Answer
Introduced as an experimental feature in Node.js 20, the Permission Model allows restricting runtime access to resources such as the file system, network, and child processes using security CLI flags.
--allow-fs-read/--allow-fs-write: Limit folder/file access.
--allow-child-process: Disable/restrict spawning child processes.
Interviewer Note: 💡 Showcases that the candidate stays current with Node.js release cycles.
Senior Question #60: How do you implement structured logging in a production Node.js application?
Level: Senior | Category: Performance & Diagnostics | Time: 9 mins
Expected Answer
In production, console.log should be avoided because it blocks synchronously when outputting to standard out. Use structured loggers like pino or winston that output log events as JSON formats, enabling quick indexing in Logstash/Elasticsearch aggregators.
Code Example
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
mixin() { return { correlationId: 'tx-123' }; }
});
logger.info({ userId: 456 }, 'User successfully authenticated');
Interviewer Note: 💡 Ask: Why JSON formats? (To parse structured metadata values cleanly in logging pipelines).
Senior Question #61: Explain SQL Injection (SQLi) and how to prevent it in raw queries.
Level: Senior | Category: Security | Time: 9 mins
Expected Answer
SQLi occurs when untrusted user input is directly concatenated into SQL strings, allowing malicious payloads to alter the query command structure. Prevent it by using **Prepared Statements** (parameterized queries) or modern ORMs, separating query structure from variables.
Code Example
// VULNERABLE
const query = `SELECT * FROM users WHERE email = '${email}'`;
// SECURE
const query = 'SELECT * FROM users WHERE email = ?';
const [rows] = await db.execute(query, [email]);
Interviewer Note: 💡 Verify they understand parameterized placeholders are treated strictly as literals, not executable commands.
Senior Question #62: How do you prevent XSS and CSRF attacks in Express applications?
Level: Senior | Category: Security | Time: 11 mins
Expected Answer
Prevention strategies:
- XSS (Cross-Site Scripting): Sanitize user HTML inputs using libraries like
dompurify. Configure strict Content-Security-Policy (CSP) headers using Helmet.
- CSRF (Cross-Site Request Forgery): Store JWT/session tokens inside secure,
HttpOnly cookies configured with SameSite=Strict attribute. Require CSRF tokens on state-changing requests (POST/PUT/DELETE).
Interviewer Note: 💡 Ask: What does HttpOnly cookie do? (Prevents client-side scripts from reading the token).
Senior Question #63: What is the Event Loop block detector and how do you prevent loop starvation?
Level: Senior | Category: Event Loop | Time: 11 mins
Expected Answer
Event loop block/starvation occurs when synchronous operations (long JSON.parse, loops, crypto) hog the main execution thread, preventing microtasks and callbacks from running. You can detect blocking behavior using tools like `blocked-at` or node diagnostics hooks.
Code Example
// Split heavy work with setImmediate
function processChunks(data) {
if (data.length === 0) return;
doSmallSegment(data.shift());
// Defer next segment to prevent loop blocking
setImmediate(() => processChunks(data));
}
Interviewer Note: 💡 Seniors must prioritize breaking up synchronous CPU tasks.
Senior Question #64: How does DNS lookup resolution work in Node.js and its connection pool?
Level: Senior | Category: Event Loop | Time: 10 mins
Expected Answer
Node.js core methods like dns.lookup() call the system-level C API getaddrinfo(), which is a blocking operation. Node handles this by running the lookup inside the Libuv thread pool. If the thread pool is full, DNS lookups stall, adding latency to HTTP requests.
To avoid this dependency, use dns.resolve() instead, which runs asynchronously over network sockets, bypassing the libuv threads.
Interviewer Note: 💡 Excellent optimization topic for high-concurrency external API requests.
Senior Question #65: How do you write a custom Transform stream in Node.js?
Level: Senior | Category: Streams & Buffers | Time: 11 mins
Expected Answer
A Transform stream is a type of Duplex stream where the output is computed from the input. You write it by extending the Transform class from stream module and overriding the _transform() method.
Code Example
const { Transform } = require('stream');
class UpperCaseTransform extends Transform {
_transform(chunk, encoding, callback) {
const upperCased = chunk.toString().toUpperCase();
this.push(upperCased);
callback(); // Indicate processing is complete
}
}
process.stdin.pipe(new UpperCaseTransform()).pipe(process.stdout);
Interviewer Note: 💡 Candidates should explain why chunk conversion chunk.toString() requires encoding safety.
Senior Question #66: What are best practices for structuring a production-ready Node.js monorepo?
Level: Senior | Category: Design Patterns | Time: 11 mins
Expected Answer
Monorepo best practices:
- Use package managers with built-in workspace support (npm workspaces, pnpm, yarn).
- Separate core shared business logic (e.g. database schemas, authentication middleware) into isolated local packages referenced by apps.
- Utilize monorepo orchestration tools (turborepo, nx) to cache builds, lint, and run tests globally.
Interviewer Note: 💡 High-quality candidates focus on build caching and module isolation.
Senior Question #67: How do you configure PM2 for zero-downtime hot reloads in cluster mode?
Level: Senior | Category: Performance & Diagnostics | Time: 10 mins
Expected Answer
PM2 cluster mode creates child instances to scale Node across multiple CPU cores. For zero-downtime reloads, configure PM2 with reload instead of restart, and configure `listen_timeout` to wait for app boot verification.
Code Example
// ecosystem.config.js
module.exports = {
apps: [{
name: "app",
script: "./app.js",
instances: "max", // Scale across all available CPU cores
exec_mode: "cluster",
listen_timeout: 3000,
wait_active: true
}]
};
Interviewer Note: 💡 Candidate should explain that reload spawns new processes before killing old ones.
Senior Question #68: How do you build multi-stage Docker builds for Node.js applications?
Level: Senior | Category: Scaling & Infrastructure | Time: 11 mins
Expected Answer
Multi-stage Docker builds separate build tool chains (webpack, ts-compilers) from the runtime runtime image to minimize target container footprint and security attack surface.
Code Example
# Build Stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production Stage
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=build /app/dist ./dist
CMD ["node", "dist/main.js"]
Interviewer Note: 💡 Alpine base tags reduce final sizes from 1GB to ~100MB.
Senior Question #69: Explain how prototype pollution works in Node.js and how to defend against it.
Level: Senior | Category: Security | Time: 10 mins
Expected Answer
Prototype pollution is a vulnerability where attackers inject properties into the JavaScript base object prototype (Object.prototype) using keys like __proto__ or constructor.prototype, altering behavior in all created objects.
Defense: Use Object.create(null) for dictionary objects, schema validate user payload properties, or use Object.freeze(Object.prototype) on startup.
Interviewer Note: 💡 Classic JavaScript/Node.js security round question.
Senior Question #70: How do you implement OAuth 2.0 PKCE flow in a Node.js API?
Level: Senior | Category: Security | Time: 11 mins
Expected Answer
PKCE (Proof Key for Code Exchange) secures client authentication. The client generates a high-entropy cryptographically secure string called code_verifier, hashes it to create `code_challenge`, and sends the challenge on initial authorization. On redirect token retrieval, the client sends the original verifier, which Node validates by re-hashing.
Interviewer Note: 💡 Essential flow for mobile and single page app authentication integrations.
Architect Question #71: Design a distributed rate limiter that scales across a cluster of 50 Node.js API servers.
Level: Architect | Category: Scaling & Infrastructure | Time: 15 mins
Expected Answer
A local memory limiter fails in clusters because traffic is spread randomly. A distributed rate limiter leverages a central Redis cache using a **sliding window counter** pattern. To minimize network latency, we implement the rate checking logic inside a **Redis Lua Script**, ensuring atomic executions in a single network round-trip.
Code Example
// Lua script for sliding window rate limiting
const rateLimitLua = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local clearBefore = now - window
redis.call('zremrangebyscore', key, 0, clearBefore)
local amount = redis.call('zcard', key)
if amount < limit then
redis.call('zadd', key, now, now)
redis.call('expire', key, window)
return 1
else
return 0
end
`;
Interviewer Note: 💡 Candidate should discuss Redis clustering, failover, and local-cache fallbacks in case Redis goes offline.
Architect Question #72: Design a system architecture to handle 100,000 concurrent WebSocket connections.
Level: Architect | Category: Scaling & Infrastructure | Time: 15 mins
Expected Answer
High-concurrency WebSocket systems require a horizontally scaled layer of Node.js instances backed by a Redis Pub/Sub backplane. Key components include:
- Load Balancing: An Nginx or AWS ALB layer configured with IP Hash (sticky sessions) to upgrade TCP socket connections.
- Horizontal Scale: Scaling Node instances across clusters, keeping connections alive by modifying OS limits (
nofile setting to 200,000+ files).
- Pub/Sub Sync: When a user on Server A messages a user on Server B, the message publishes to a Redis channel, prompting Server B to broadcast to the targeted client.
Interviewer Note: 💡 Highlight the difference in memory footprints per connection: tuning socket timeout parameters to prune inactive sockets.
Architect Question #73: How do you design a secure microservices authentication architecture using API Gateways?
Level: Architect | Category: Scaling & Infrastructure | Time: 15 mins
Expected Answer
Authentication should be centralized at the API Gateway layer (e.g. Kong, Apigee, or a custom Express gateway). The gateway verifies incoming JWTs or OAuth tokens. Upon successful validation, it injects custom header attributes (such as x-user-id, x-user-roles) and routes the request to target downstream services. Downstream microservices remain stateless and trust requests that match internal network authorization signatures.
Interviewer Note: 💡 Architect candidates should point out that this pattern isolates microservices from auth details.
Architect Question #74: Design a distributed background job queue system using BullMQ and Redis.
Level: Architect | Category: Design Patterns | Time: 15 mins
Expected Answer
Background processors avoid blocking client-facing API responses. BullMQ implements a producer-consumer model using Redis streams and sorting sets to support job retries, delays, prioritization, concurrency parameters, and concurrency limits.
Code Example
const { Queue, Worker } = require('bullmq');
// Producer side (in Express API controller)
const emailQueue = new Queue('emails', { connection: redisConfig });
await emailQueue.add('welcomeEmail', { email: 'vijay@example.com' });
// Consumer side (in Worker process node)
const worker = new Worker('emails', async (job) => {
await sendEmail(job.data.email);
}, { connection: redisConfig, concurrency: 10 });
Interviewer Note: 💡 Discuss how to handle task failures and prevent infinite loops with dead letter queues.
Architect Question #75: How would you implement database sharding and dynamic query routing in Node.js?
Level: Architect | Category: Design Patterns | Time: 14 mins
Expected Answer
Database sharding partitions tables horizontally across separate database engines. Dynamic routing calculates shard destinations based on a partition key (e.g. userId % totalShards). Node handles routing dynamically by instantiating database pools for each shard and routing queries inside middleware/repositories using the computed shard key.
Interviewer Note: 💡 Focus on how schema modifications and distributed joins become complex when databases are sharded.
Architect Question #76: Design a telemetry, monitoring, and observability pipeline for a Node.js ecosystem.
Level: Architect | Category: Performance & Diagnostics | Time: 15 mins
Expected Answer
A robust observability stack relies on three pillars: Metrics, Traces, and Logs.
- Metrics: Expose Prometheus metrics (/metrics endpoint) capturing Node process states (event loop delay, heap size, active handles) using
prom-client.
- Traces: Instrument applications with
OpenTelemetry (OTel) SDK. Inject span contexts on external queries to enable distributed trace charts (Jaeger, Zipkin).
- Logs: Stream JSON structured logs to Elasticsearch/Logstash aggregator layers.
Interviewer Note: 💡 Ask: How does event loop delay monitoring help? (It measures if synchronous operations are blocking the loop).
Architect Question #77: Design a zero-downtime deployment strategy for Node.js APIs in Kubernetes.
Level: Architect | Category: Scaling & Infrastructure | Time: 15 mins
Expected Answer
Zero-downtime relies on Kubernetes **Rolling Updates** combined with proper Node.js shutdown hook listeners. Key configurations include:
- Readiness Probe: Monitors if Node is booted and databases are connected before directing traffic.
- Liveness Probe: Monitors if the process crashed or hung.
- SIGTERM Handler: Node catches SIGTERM, immediately closes HTTP servers (stopping new connections), waits for active requests to finish (graceful shutdown window), and exits.
Code Example
const server = app.listen(3000);
process.on('SIGTERM', () => {
console.log('SIGTERM received. Starting graceful shutdown...');
server.close(() => {
console.log('HTTP server closed. Exiting process.');
db.closeConnection().then(() => process.exit(0));
});
});
Interviewer Note: 💡 Crucial: Candidate should know that process.exit(0) must not run immediately upon SIGTERM to prevent dropping active TCP sockets.
Architect Question #78: Explain clean architecture folder structure and module isolation in Enterprise Node.js.
Level: Architect | Category: Design Patterns | Time: 14 mins
Expected Answer
Clean Architecture enforces separation of concerns by placing business logic at the core, independent of external frameworks (Express, NestJS, databases, ORMs). The folder layers are:
- Domain (Entities): Central business models and logic (pure JS/TS).
- Use Cases (Application): Orchestrates domain models. Declares interface abstractions for external integrations.
- Interface Adapters (Controllers, Repositories): Converts HTTP or database inputs into domain models.
- Frameworks & Drivers: External frameworks (Express router files, Sequelize/Prisma config pools).
Interviewer Note: 💡 Red Flag: Importing database drivers or ORM files inside domain entities.
Architect Question #79: How do you handle distributed transactions and eventual consistency in Node.js microservices?
Level: Architect | Category: Scaling & Infrastructure | Time: 15 mins
Expected Answer
Two-Phase Commit (2PC) blocks databases and fails at scale. We solve this by implementing the **Saga Pattern** (orchestrated or choreographed) or **Outbox Pattern**.
- Saga: Each service executes a local transaction and publishes an event. If a downstream service fails, compensating transactions are triggered backwards to roll back previous steps.
- Outbox: Saves database records and outbox events in a single transaction, publishing events to Kafka via an independent publisher process to ensure delivery.
Interviewer Note: 💡 Essential for large-scale systems design interviews.
Architect Question #80: Design a scalable real-time notification engine supporting push, SMS, and email.
Level: Architect | Category: Design Patterns | Time: 15 mins
Expected Answer
A scalable notification platform uses an event-driven architecture based on **Apache Kafka** or **RabbitMQ** combined with BullMQ processing layers. Key stages include:
- Ingestion API: Express endpoint validates requests and immediately pushes a job event (e.g.
SendNotification) into a message broker, returning status 202 accepted.
- Broker (Kafka): Partitions notification events by user or category to scale consumers.
- Consumers (BullMQ): Specialized worker groups subscribe to events, retrieve user profile settings, apply template designs, and call third-party services (Twilio, Sendgrid) with exponential retries.
Interviewer Note: 💡 Architect should explain how to implement rate limits per provider so target APIs don't block the workers.