Architecture & Design Patterns
15 questions foundCQRS separates the models used for writing data (commands, which change state) from the models used for reading data (queries, which return state), often backed by entirely different data representations or even different databases optimized for each purpose -- this is valuable when read and write workloads have very different scaling or shape requirements (like a system with heavy, complex reporting queries but simple writes), but it's genuinely overkill for most CRUD applications, adding synchronization complexity (especially if paired with eventual consistency between the write and read models) that isn't justified without a real need.
// Command side: normalized write model
async function createOrder(command) {
await db.orders.insertOne({ id: command.id, items: command.items, status: 'pending' });
eventBus.publish('OrderCreated', command);
}
// Query side: denormalized read model, updated asynchronously via the event
eventBus.subscribe('OrderCreated', async (event) => {
await readDb.orderSummaries.insertOne({ id: event.id, itemCount: event.items.length });
});
Real-world example An analytics-heavy e-commerce platform maintains a separate, denormalized read database optimized for dashboard queries, updated asynchronously whenever the normalized write database records a new order, so complex reporting queries never compete for resources with the transactional order-placement path.
Common follow-ups: What's the specific risk of eventual consistency between the write and read models, and how do you communicate that to end users?;What's a simpler intermediate step short of full CQRS for a team not ready for that complexity?
Databases & ORMs (MongoDB/Mongoose SQL/Sequelize);Message Queues (RabbitMQ & Kafka)
Dependency Injection means a component receives its dependencies (other objects or functions it needs) from the outside, typically via constructor or function parameters, rather than creating or looking them up itself -- this decouples the component from specific implementations and makes substituting mocks or fakes during testing straightforward. Node.js has no built-in DI container the way some other languages do, so DI is commonly implemented manually via constructor parameters and a manual composition step (sometimes called 'poor man's DI'), or via a small library like Awilix, InversifyJS, or tsyringe for larger applications.
// Manual DI via constructor parameters -- no framework needed
class OrderService {
constructor(orderRepository, notificationService) {
this.orderRepository = orderRepository;
this.notificationService = notificationService;
}
}
// Composition root: where all the wiring happens, typically once at startup
const orderService = new OrderService(new OrderRepository(db), new EmailNotificationService());
Real-world example A team testing their OrderService constructs it with a fake in-memory OrderRepository and a mock NotificationService in their unit tests, while the production composition root wires up the real database-backed repository and real email service -- all without changing a single line of OrderService itself.
Common follow-ups: At what team or project size does adopting a formal DI container like InversifyJS start paying off over manual wiring?;How does DI relate to and support the Dependency Inversion Principle from SOLID?
Testing with Jest Mocha & the Node Test Runner;Design Patterns in JavaScript
Because distributed systems typically can't use a single ACID database transaction spanning multiple services, the Saga pattern manages a multi-step business process as a sequence of local transactions, each in its own service, with a corresponding compensating action defined for each step to undo it if a later step fails -- implemented either as choreography (each service listens for events and reacts, fully decentralized) or orchestration (a central coordinator explicitly calls each step and triggers compensations on failure).
// Orchestration-style saga (simplified)
async function bookTripSaga(trip) {
const flight = await bookFlight(trip);
try {
const hotel = await bookHotel(trip);
try {
return await bookCar(trip);
} catch (err) { await cancelHotel(hotel); throw err; }
} catch (err) { await cancelFlight(flight); throw err; }
}
Real-world example A travel-booking platform uses an orchestrated saga to book a flight, hotel, and rental car across three separate microservices; if the car-rental booking fails after the flight and hotel already succeeded, the saga automatically triggers compensating cancelFlight and cancelHotel calls rather than leaving the customer half-booked.
Common follow-ups: What's the practical difference between choreography-based and orchestration-based sagas in terms of operational complexity?;How do you handle the case where a compensating action itself fails?
Message Queues (RabbitMQ & Kafka);Microservices Architecture with Node.js
The Strategy pattern defines a family of interchangeable algorithms behind a common interface, letting the calling code select and swap the concrete implementation at runtime without changing its own logic -- for a checkout service supporting Stripe, PayPal, and bank transfers, each provider implements the same processPayment() interface, and the checkout flow simply calls whichever strategy was selected based on the customer's chosen payment method.
class StripeStrategy { async pay(amount) { return stripeClient.charge(amount); } }
class PayPalStrategy { async pay(amount) { return paypalClient.createPayment(amount); } }
class Checkout {
constructor(paymentStrategy) { this.strategy = paymentStrategy; }
async completeOrder(amount) { return this.strategy.pay(amount); }
}
new Checkout(new StripeStrategy()).completeOrder(4999);
Real-world example An online store adds Apple Pay support months after launch by writing a single new ApplePayStrategy class implementing the existing pay() interface, without touching the Checkout class or any of the other existing payment strategies at all.
Common follow-ups: How does the Strategy pattern differ from simply using an if/else chain based on payment type?;How would you make the choice of strategy configurable per merchant rather than hardcoded?
Design Patterns in JavaScript;Payments
A Facade provides a single, simplified interface over a more complex subsystem of classes or libraries -- rather than every part of an application directly importing and configuring multiple third-party SDKs (payment, email, SMS, analytics) with their own inconsistent APIs, a Facade wraps them behind one consistent internal interface, isolating the rest of the codebase from those libraries' specific quirks and making it easier to later swap out or upgrade an underlying SDK.
class NotificationFacade {
constructor() { this.email = new SendGridClient(); this.sms = new TwilioClient(); }
async notifyUser(user, message) {
if (user.prefersEmail) return this.email.send(user.email, message);
return this.sms.send(user.phone, message);
}
}
// Rest of the app only ever calls this simple interface
await notificationFacade.notifyUser(user, 'Your order has shipped');
Real-world example An application originally calling the Twilio SDK directly from a dozen different files is refactored to go through a single NotificationFacade, so that a later migration from Twilio to a different SMS provider only requires changing that one facade class instead of a dozen call sites.
Common follow-ups: What's the difference between a Facade and an Adapter pattern, given both wrap another interface?;At what point does a facade risk becoming an unwieldy 'god object' itself?
Design Patterns in JavaScript;Email & Notifications