Welcome back to The Everyday Backend Engineer: Practical Design Patterns. So far, we have explored the Singleton and Factory patterns. Today, we are closing out the Creational Patterns family by looking at a structural lifesaver for handling massive configurations: The Builder Pattern.
Let’s see how to transform unreadable constructor parameters into clean, fluent, and highly readable API builders.
🔴 The Problem: The Telescoping Constructor (Parameter Hell)
Imagine you are writing a custom SQL query generator or a complex reporting system module for your application. When someone wants to query data, they might need to specify sorting, filtering conditions, grouping, limits, and joins. Most of these options are completely optional.
If you use a traditional constructor setup to handle this, your object initialization instantly becomes an unreadable nightmare:
// ❌ Bad Practice: Forcing long, ordered positional arguments with "null" values
const sqlQuery = new CustomQueryGenerator(
"users",
null,
null,
["id", "name", "email"],
null,
"status = 'active'",
10
);
Why is this broken?
Looking at that instantiation, it is completely impossible to decipher what the third null or the number 10 represents without opening up the base class implementation.
If you mess up the parameter positioning by just one slot, your entire application crashes silently with bad logic.
🟢 The Solution: Step-by-Step Object Assembly
Instead of forcing your code to build the entire complex object in a single massive constructor blast, the Builder Pattern lets you build the object step-by-step.
Each configuration option is broken out into its own dedicated method that updates the internal state and then returns this (the builder instance itself). This allows you to chain methods fluidly until you are ready to call a final .build() method to output the fully assembled product.
🥪 The Real-World Analogy: "The Subway Sandwich"
Think of ordering a sandwich at a Subway counter. You don't just walk up and yell a single, hyper-specific 15-word structural sandwich sentence. Instead, you build it sequentially:
"Give me wheat bread" -> "Add toasted chicken" -> "Double up on Swiss cheese" -> "Pour Southwest sauce".
At the very end of the line, the final assembly step puts everything together and wraps it up for you to take home.
💻 The Clean Node.js Implementation
Let’s build a clean, highly extensible RequestQueryBuilder that eliminates parameter clutter entirely.
// builders/queryBuilder.js
class RequestQueryBuilder {
constructor(tableName) {
// Core mandatory property goes into the constructor
this.table = tableName;
// Optional properties initialized with clean defaults
this.fields = '*';
this.conditions = [];
this.rowsLimit = null;
this.sortBy = null;
}
// Step 1: Define what fields to retrieve
select(fieldsArray) {
this.fields = fieldsArray.join(', ');
return this; // 🎯 Returning 'this' enables Method Chaining
}
// Step 2: Add query constraints dynamically
where(condition) {
this.conditions.push(condition);
return this;
}
// Step 3: Add sorting constraints
orderBy(columnDirection) {
this.sortBy = columnDirection;
return this;
}
// Step 4: Cap the pagination limits
limit(number) {
this.rowsLimit = number;
return this;
}
// 🏁 The Final Step: Assemble and validate the final product
build() {
let sql = `SELECT ${this.fields} FROM ${this.table}`;
if (this.conditions.length > 0) {
sql += ` WHERE ${this.conditions.join(' AND ')}`;
}
if (this.sortBy) {
sql += ` ORDER BY ${this.sortBy}`;
}
if (this.rowsLimit) {
sql += ` LIMIT ${this.rowsLimit}`;
}
return sql;
}
}
module.exports = RequestQueryBuilder;
🎯 How to use it fluidly inside your controllers
Now, instead of passing blind null values into a messy constructor, look at how beautifully declarative and readable your code becomes through method chaining:
// controllers/reportController.js
const RequestQueryBuilder = require('../builders/queryBuilder');
async function getActiveUsersReport(req, res) {
// Elegant, self-documenting step-by-step assembly
const finalizedSQLQuery = new RequestQueryBuilder("users")
.select(['id', 'username', 'email'])
.where("status = 'active'")
.where("age >= 18")
.orderBy("created_at DESC")
.limit(25)
.build();
console.log(finalizedSQLQuery);
// Output: SELECT id, username, email FROM users WHERE status = 'active' AND age >= 18 ORDER BY created_at DESC LIMIT 25
// Execute query via your database client...
res.json({ query: finalizedSQLQuery });
}
🧠 Code Smell Test: When should you use a Builder?
Keep an eye out for these structural signals:
-
The Smell: An instantiation statement forces developers to pass multiple positional parameters or sequential
null/undefinedfallbacks to handle optional features. -
The Fix: Move the optional configurations out of the initial constructor parameters and create individual fluent configuration methods that return
this.
That wraps up Step 3! We have successfully finished exploring the Creational Patterns family.
Up next, we enter Category 2: Structural Patterns with Step 4: Dependency Injection (DI), where we will uncover how to untangle our service layers and make our modules infinitely testable.
Top comments (0)