Skip to main content

Factory Pattern

Factory Pattern in JS

The Factory Pattern is one of the most common creational design patterns in Node.js.

Think of it like this:

"I don't want to worry about how an object is created. I just want the right one."


What is Factory Pattern?

It is a creational design pattern where:

  • you delegate object creation to a factory function or class
  • the caller doesn't need to know the concrete class being instantiated
  • the factory decides which object to create based on input

Real-world analogy

Think of a logistics company:

  • You call and say: "I need to ship this parcel"
  • The company decides internally: "Use Dunzo for same-day, Shadowfax for next-day"

You don't care about the implementation — the factory picks the right one.


Types of Factory Pattern

1. Simple Factory

A plain function or object map that returns the right implementation.

2. Factory Method Pattern

Subclasses decide which object to create.

3. Abstract Factory Pattern

Creates families of related objects.

We'll focus on Simple Factory and Factory Method as they're most common in Node.js.


Simple Factory — Function Style

The most idiomatic JS approach: a function that returns the right object.

Problem: Create different types of notifications

class EmailNotification {
send(message) {
console.log(`Email: ${message}`);
}
}

class SmsNotification {
send(message) {
console.log(`SMS: ${message}`);
}
}

class PushNotification {
send(message) {
console.log(`Push: ${message}`);
}
}

Simple Factory Function

function createNotification(type) {
switch (type) {
case 'email': return new EmailNotification();
case 'sms': return new SmsNotification();
case 'push': return new PushNotification();
default: throw new Error(`Unknown notification type: ${type}`);
}
}

Usage

const notification = createNotification('email');
notification.send('Your order has been shipped!');
// Email: Your order has been shipped!

Simple Factory — Map Style (preferred)

Using a Map or object instead of switch is more scalable — adding a new type requires no changes to the factory logic.

const notificationClasses = {
email: EmailNotification,
sms: SmsNotification,
push: PushNotification,
};

function createNotification(type) {
const NotificationClass = notificationClasses[type];
if (!NotificationClass) throw new Error(`Unknown notification type: ${type}`);
return new NotificationClass();
}

Adding a new type:

notificationClasses.whatsapp = WhatsAppNotification;
// That's it. No touching the factory function.

Factory Method Pattern

Problem: Different loggers for different environments

The Factory Method Pattern uses inheritance — subclasses override a factory method.

Base class with factory method

class LoggerFactory {
// Subclasses override this
createLogger() {
throw new Error('createLogger() must be implemented by subclass');
}

log(message) {
const logger = this.createLogger();
logger.write(message);
}
}

Concrete loggers

class ConsoleLogger {
write(message) {
console.log(`[CONSOLE] ${message}`);
}
}

class FileLogger {
write(message) {
console.log(`[FILE] Writing to file: ${message}`);
}
}

class CloudLogger {
write(message) {
console.log(`[CLOUD] Sending to CloudWatch: ${message}`);
}
}

Concrete factories

class DevelopmentLoggerFactory extends LoggerFactory {
createLogger() {
return new ConsoleLogger();
}
}

class ProductionLoggerFactory extends LoggerFactory {
createLogger() {
return new CloudLogger();
}
}

Usage based on environment

const factory = process.env.NODE_ENV === 'production'
? new ProductionLoggerFactory()
: new DevelopmentLoggerFactory();

factory.log('Server started on port 3000');
// Dev: [CONSOLE] Server started on port 3000
// Prod: [CLOUD] Sending to CloudWatch: Server started on port 3000

Real backend use cases in Node.js

1) Database connection factory

class PostgresClient {
async query(sql) {
console.log(`Postgres: ${sql}`);
}
}

class MongoClient {
async query(query) {
console.log(`Mongo: ${JSON.stringify(query)}`);
}
}

const dbClients = {
postgres: PostgresClient,
mongo: MongoClient,
};

function createDbClient(type) {
const Client = dbClients[type];
if (!Client) throw new Error(`Unknown DB: ${type}`);
return new Client();
}

// Usage
const db = createDbClient(process.env.DB_TYPE || 'postgres');
await db.query('SELECT * FROM orders');

2) Report generator factory

class CsvReportGenerator {
generate(data) {
return data.map(row => Object.values(row).join(',')).join('\n');
}
}

class JsonReportGenerator {
generate(data) {
return JSON.stringify(data, null, 2);
}
}

class PdfReportGenerator {
generate(data) {
// pdf generation logic
return `PDF with ${data.length} rows`;
}
}

const reportGenerators = {
csv: CsvReportGenerator,
json: JsonReportGenerator,
pdf: PdfReportGenerator,
};

function createReportGenerator(format) {
const Generator = reportGenerators[format];
if (!Generator) throw new Error(`Unsupported format: ${format}`);
return new Generator();
}

// Usage in Express route
app.get('/report', (req, res) => {
const generator = createReportGenerator(req.query.format);
const output = generator.generate(data);
res.send(output);
});

3) Queue job factory

class EmailJob {
async process(payload) {
await emailService.send(payload.to, payload.subject, payload.body);
}
}

class SmsJob {
async process(payload) {
await smsService.send(payload.phone, payload.text);
}
}

class ExportJob {
async process(payload) {
await exportService.generate(payload.reportId);
}
}

const jobHandlers = {
send_email: EmailJob,
send_sms: SmsJob,
export_report: ExportJob,
};

function createJobHandler(jobType) {
const Handler = jobHandlers[jobType];
if (!Handler) throw new Error(`Unknown job type: ${jobType}`);
return new Handler();
}

// Queue consumer
async function processJob(job) {
const handler = createJobHandler(job.type);
await handler.process(job.payload);
}

Registering factories dynamically

For plugin-based systems, let external code register new types:

class NotificationFactory {
#registry = new Map();

register(type, NotificationClass) {
this.#registry.set(type, NotificationClass);
}

create(type) {
const Cls = this.#registry.get(type);
if (!Cls) throw new Error(`Unknown notification type: ${type}`);
return new Cls();
}
}

const factory = new NotificationFactory();
factory.register('email', EmailNotification);
factory.register('sms', SmsNotification);

// Any module can register its own type
factory.register('slack', SlackNotification);

const n = factory.create('slack');
n.send('Deployment done!');

This is the open registration pattern — the factory doesn't need to know about every type upfront.


Benefits

1. Centralizes object creation

All creation logic in one place. Easy to change.


2. Hides implementation details

Caller doesn't need to import concrete classes.


3. Easy to extend

Add a new type without touching the factory function (with the Map pattern).


4. Improves testability

Inject a mock factory in tests.

// In test
const mockFactory = { create: () => ({ send: jest.fn() }) };

Factory vs Strategy Pattern

Factory Pattern — focuses on object creation

"Which object should I create?"

Strategy Pattern — focuses on behavior at runtime

"How should this task be performed?"

In real apps they are used together:

const strategy = carrierFactory.create(carrier); // Factory
await strategy.createShipment(order); // Strategy

When to use Factory Pattern

  • when object creation logic is complex or conditional
  • when you want to decouple object creation from usage
  • when the type of object isn't known until runtime (from DB, config, query param)
  • when you want a plugin/registry system

When NOT to use it

  • when you only ever create one type of object — just use new directly
  • when adding a factory adds complexity without benefit

Interview definition (short answer)

"Factory Pattern is a creational design pattern that provides an interface for creating objects without specifying their exact class. The factory decides which concrete implementation to instantiate based on input."


Formula:

Input/Config → Factory → Correct Object