Architecture & Design Patterns

15 questions found

What is the Module pattern in Node.js, and how does it enable encapsulation?

Intermediate
The Module pattern uses each file's own function scope (whether via CommonJS's implicit wrapper function or ESM's module scope) to keep variables and functions private by default, exposing only what's explicitly attached to module.exports or exported via 'export' -- this gives every Node.js file natural encapsulation without needing IIFEs or closures written by hand, unlike plain browser scripts sharing a single global scope.
// counter.js
let count = 0; // private, invisible outside this file

function increment() { count += 1; return count; }
function getCount() { return count; }

module.exports = { increment, getCount };
Real-world example A payment library exposes only a processPayment() function while keeping its internal retry logic, API keys, and validation helpers completely private to the module, preventing consuming applications from accidentally depending on or mutating implementation details that might change between versions.

Common follow-ups: How does this differ from using a class with private fields (#field) for the same encapsulation goal?;What happens to module-level state when the same module is required from multiple files?

Modules (CommonJS/ESM);OOP & Classes

How is the Factory pattern commonly applied in Node.js applications, and what problem does it solve?

Intermediate
A factory function or factory class centralizes object creation logic behind a single function, hiding the specific class or configuration details from the calling code -- useful when object construction is conditional (different implementations based on environment or input) or complex enough that scattering 'new ClassName()' calls throughout the codebase would create tight coupling to specific implementations.
function createLogger(env) {
  if (env === 'production') return new JsonLogger();
  if (env === 'test') return new SilentLogger();
  return new ConsoleLogger();
}

const logger = createLogger(process.env.NODE_ENV);
Real-world example A multi-tenant SaaS application uses a factory function to construct the correct database adapter (PostgresAdapter, MySQLAdapter, or a mock TestAdapter) based on each tenant's configuration, so the rest of the application code interacts only with a common adapter interface without knowing which concrete class it received.

Common follow-ups: How does the Factory pattern relate to the Dependency Injection pattern -- are they complementary or competing approaches?;When does introducing a factory add unnecessary indirection for a genuinely simple case?

Design Patterns in JavaScript;Dependency Injection & IoC Principles

What is the Singleton pattern, and what are the specific risks of using it in a Node.js application?

Intermediate
A Singleton ensures only one instance of a class or module-level object exists and is shared across the entire application -- in Node.js this is trivially achieved because a required module is cached and reused by reference after its first load, so a module that exports a single instantiated object naturally behaves as a singleton. The main risk is that singletons introduce hidden global state, making unit tests harder to isolate (state can leak between tests) and creating implicit coupling between unrelated parts of the codebase that all depend on the same shared instance.
// database.js -- naturally a singleton due to module caching
class Database {
  constructor() { this.connection = connectToDb(); }
}
module.exports = new Database(); // same instance returned on every require()
Real-world example A connection-pool manager is implemented as a module-level singleton so that every part of an application shares the same underlying pool of database connections, but this later complicates testing until the team refactors to inject the pool explicitly rather than importing the singleton directly.

Common follow-ups: How would you refactor a singleton-based module to be more testable via dependency injection?;What happens to a 'singleton' module if it's required from two different node_modules copies of the same package?

Design Patterns in JavaScript;Modules (CommonJS/ESM)

What is a layered (n-tier) architecture in a Node.js/Express application, and what are its typical layers?

Advanced
A layered architecture separates an application into distinct responsibilities, commonly: the routing/controller layer (handling HTTP requests and responses), the service/business-logic layer (implementing the actual application rules), the data-access/repository layer (talking to the database), and the model layer (data shape definitions) -- each layer depends only on the layer below it, which improves testability (business logic can be tested without an HTTP server or a real database) and makes it easier to swap implementations, like changing databases without touching business logic.
// controller
app.post('/users', (req, res) => userService.createUser(req.body).then(u => res.json(u)));

// service (business logic)
async function createUser(data) {
  validateUser(data);
  return userRepository.save(data);
}

// repository (data access)
async function save(userData) { return db.collection('users').insertOne(userData); }
Real-world example A growing Express API refactors from having database queries directly inside route handlers to a proper repository layer, which immediately makes it possible to unit test the business logic against an in-memory fake repository instead of requiring a real database connection for every test.

Common follow-ups: How does this layered approach compare to a more feature-based ('vertical slice') folder structure?;At what point does strict layering start to add more overhead than value for a small project?

Express & Middleware;Testing with Jest Mocha & the Node Test Runner

What is the Repository pattern, and how does it help decouple business logic from a specific database technology?

Advanced
The Repository pattern defines an abstraction (an interface or a consistent set of functions) for accessing and persisting domain objects, hiding the specific database technology and query syntax behind that abstraction -- business logic calls methods like findById() or save() without knowing whether the underlying implementation uses MongoDB, PostgreSQL, or an in-memory store, which makes both testing (via a fake in-memory repository) and future database migrations significantly easier.
class UserRepository {
  async findById(id) { return db.collection('users').findOne({ _id: id }); }
  async save(user) { return db.collection('users').insertOne(user); }
}

// In tests, swap in a fake implementation with the same interface
class FakeUserRepository {
  constructor() { this.users = new Map(); }
  async findById(id) { return this.users.get(id); }
  async save(user) { this.users.set(user.id, user); return user; }
}
Real-world example A team migrating from MongoDB to PostgreSQL is able to complete the migration by rewriting only the internals of their UserRepository class, since every other part of the application only ever called repository.findById() and repository.save() and never issued raw queries directly.

Common follow-ups: How does the Repository pattern relate to and differ from an ORM's own query builder abstraction?;What's the tradeoff of introducing a repository layer for a very simple CRUD application?

Databases & ORMs (MongoDB/Mongoose SQL/Sequelize);Testing with Jest Mocha & the Node Test Runner

What is the Observer pattern, and how does Node.js's EventEmitter class implement it natively?

Advanced
The Observer pattern lets an object (the subject) maintain a list of dependents (observers) and notify them automatically of state changes, without the subject needing to know any specifics about the observers -- Node.js's built-in EventEmitter class is a direct, first-class implementation of this pattern: any object can extend EventEmitter, emit named events, and any number of listeners can subscribe to those events independently.
const EventEmitter = require('node:events');

class OrderProcessor extends EventEmitter {
  processOrder(order) {
    // ... process order ...
    this.emit('orderCompleted', order);
  }
}

const processor = new OrderProcessor();
processor.on('orderCompleted', (order) => sendConfirmationEmail(order));
processor.on('orderCompleted', (order) => updateInventory(order));
Real-world example An order-processing service emits an 'orderCompleted' event rather than directly calling sendEmail() and updateInventory() from inside the processing function, letting new behaviors (like triggering a loyalty-points update) be added later by simply attaching a new listener, without modifying the core processing logic at all.

Common follow-ups: What happens if a listener attached to an EventEmitter throws an error -- how does that affect other listeners?;How does the Observer pattern relate conceptually to the pub/sub pattern used with message queues?

Events & EventEmitter;Message Queues (RabbitMQ & Kafka)

What is Domain-Driven Design (DDD), and how do its concepts of entities, value objects, and aggregates apply to a Node.js codebase?

Advanced
Domain-Driven Design structures code around the business domain itself rather than technical layers -- an entity is an object with a distinct identity that persists over time (a User with a stable ID), a value object is defined entirely by its attributes with no identity of its own (an Address or a Money amount, where two instances with identical values are interchangeable), and an aggregate is a cluster of entities and value objects treated as a single consistency boundary, with one designated 'aggregate root' controlling all access and enforcing invariants for the whole cluster.
class Money { // value object: equality is by value, and it's immutable
  constructor(amount, currency) { this.amount = amount; this.currency = currency; Object.freeze(this); }
  equals(other) { return this.amount === other.amount && this.currency === other.currency; }
}

class Order { // entity: has identity, and acts as the aggregate root
  constructor(id) { this.id = id; this.items = []; this.total = new Money(0, 'USD'); }
  addItem(item) { this.items.push(item); this.total = this.recalculateTotal(); }
}
Real-world example An e-commerce backend models Order as an aggregate root that owns its OrderLineItems, ensuring that any modification (like adding an item) always goes through the Order object itself so that invariants like 'total must equal the sum of line items' can never be violated by code modifying a line item directly.

Common follow-ups: How much of full DDD (bounded contexts, ubiquitous language) is realistic to adopt incrementally versus needing a full rewrite?;What's the practical difference between a value object and a plain object literal in JavaScript, given JS doesn't enforce immutability by default?

OOP & Classes;Design Patterns in JavaScript

What is a Hexagonal Architecture (Ports and Adapters), and what specific benefit does it provide for a Node.js API?

Advanced
Hexagonal Architecture structures an application so the core business logic sits in the center, communicating with the outside world (databases, HTTP frameworks, message queues, third-party APIs) only through defined interfaces called 'ports', with concrete implementations ('adapters') plugged in at the edges -- this means the core business logic has zero direct dependency on Express, MongoDB, or any other specific technology, making it possible to swap the web framework or database with minimal changes to the actual business rules, and to test the core logic in complete isolation.
// Port (interface the core logic depends on)
class NotificationPort { async send(message) { throw new Error('not implemented'); } }

// Adapter (concrete implementation, plugged in at the edge)
class EmailAdapter extends NotificationPort {
  async send(message) { return emailClient.send(message); }
}

// Core business logic depends only on the port, not the concrete adapter
class OrderService {
  constructor(notificationPort) { this.notifications = notificationPort; }
  async completeOrder(order) { await this.notifications.send(`Order ${order.id} completed`); }
}
Real-world example A startup initially ships with an EmailAdapter for order notifications, then later adds an SmsAdapter and a SlackAdapter, all implementing the same NotificationPort interface, without ever having to touch the core OrderService business logic that triggers the notification.

Common follow-ups: How does Hexagonal Architecture compare to the simpler layered architecture described earlier, and when is the added complexity worth it?;What's a concrete strategy for retrofitting this pattern onto an existing, tightly-coupled Express application?

Dependency Injection & IoC Principles;Testing with Jest Mocha & the Node Test Runner

What is the Middleware (Chain of Responsibility) pattern as implemented by Express, and how does the 'next()' function drive it?

Intermediate
Express middleware implements the Chain of Responsibility pattern: each middleware function receives the request, response, and a 'next' callback, and can either handle the request completely, modify it and pass control to the next function in the chain by calling next(), or short-circuit the chain by sending a response directly -- this lets cross-cutting concerns (authentication, logging, error handling) be composed as small, independent, reorderable functions rather than one large monolithic handler.
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next(); // pass control to the next middleware in the chain
});

app.use((req, res, next) => {
  if (!req.headers.authorization) return res.status(401).end(); // short-circuits the chain
  next();
});
Real-world example An API composes a request-logging middleware, an authentication middleware, and a rate-limiting middleware as three separate, independently testable functions applied in sequence, rather than combining all three concerns into a single large function at the top of every route handler.

Common follow-ups: What happens if a middleware function neither calls next() nor sends a response?;How does error-handling middleware (with four parameters) fit into this same chain-of-responsibility model?

Express & Middleware;Error Handling

What is a Circuit Breaker pattern, and why is it important when a Node.js service calls an unreliable downstream dependency?

Advanced
A Circuit Breaker wraps calls to an external dependency and tracks recent failure rates -- when failures exceed a threshold, the breaker 'opens' and immediately rejects further calls (without even attempting them) for a cooldown period, preventing a struggling downstream service from being overwhelmed further and preventing the calling service from wasting resources on calls likely to fail or time out; after the cooldown, it allows a limited number of test requests through in a 'half-open' state to check if the dependency has recovered.
const CircuitBreaker = require('opossum');

const breaker = new CircuitBreaker(callExternalApi, {
  timeout: 3000,
  errorThresholdPercentage: 50,
  resetTimeout: 30000,
});

breaker.fallback(() => ({ cached: true, data: getCachedResponse() }));
breaker.fire(requestParams).then(handleResponse);
Real-world example A checkout service calling a flaky third-party shipping-rate API wraps that call in a circuit breaker with a fallback to a cached flat rate, so that when the shipping API starts timing out under its own load, the checkout flow degrades gracefully instead of piling up slow, doomed requests.

Common follow-ups: How do you choose appropriate threshold and cooldown values for a circuit breaker in a real system?;How does a circuit breaker interact with retry logic -- should they be used together or is that redundant?

Error Handling;Cloud & DevOps

Showing 1–10 of 15