Back to blog

Factory and Abstract Factory Patterns Explained

oopcreational-patternsdesign-patternstypescriptpythonjava
Factory and Abstract Factory Patterns Explained

Introduction

The Singleton pattern ensures you have one instance. But what about creating many instances — and doing it in a flexible, decoupled way?

The Factory family of patterns is about delegating object creation. Instead of calling new SpecificClass() directly, you call a factory that decides which class to instantiate. This keeps your calling code independent of the concrete classes it uses.

There are two distinct patterns in this family:

PatternWhat it does
Factory MethodDefines an interface for creating an object, but lets subclasses decide which class to instantiate
Abstract FactoryProvides an interface for creating families of related objects without specifying their concrete classes

What You'll Learn

✅ Understand the problem both patterns solve
✅ Implement Factory Method in TypeScript, Python, and Java
✅ Implement Abstract Factory in TypeScript, Python, and Java
✅ Know when to use Factory Method vs Abstract Factory
✅ Explore real-world use cases: UI toolkits, notification systems, parsers
✅ Avoid common anti-patterns around factory misuse

Prerequisites


The Problem: Why Not Just Use new?

Consider a notification system:

// ❌ Tightly coupled — hard to extend
class NotificationService {
  send(type: string, message: string) {
    if (type === 'email') {
      const email = new EmailNotification();
      email.send(message);
    } else if (type === 'sms') {
      const sms = new SMSNotification();
      sms.send(message);
    } else if (type === 'push') {
      const push = new PushNotification();
      push.send(message);
    }
    // Every new type requires modifying this class ❌
  }
}

Problems with this approach:

  1. Open/Closed Principle violated — every new notification type requires modifying NotificationService
  2. Tight couplingNotificationService knows about every concrete class
  3. Hard to test — you can't inject a mock without hacking the internals
  4. Duplicated logic — if other services also create notifications, this if/else is repeated everywhere

The Factory patterns solve this by centralizing and abstracting the object creation logic.


Part 1: Factory Method Pattern

Intent

Define an interface for creating an object, but let subclasses (or implementations) decide which class to instantiate. The factory method defers instantiation to subclasses.

TypeScript Implementation

// Product interface
interface Notification {
  send(message: string): void;
  getChannel(): string;
}
 
// Concrete products
class EmailNotification implements Notification {
  constructor(private readonly recipient: string) {}
 
  send(message: string): void {
    console.log(`📧 Email to ${this.recipient}: ${message}`);
  }
 
  getChannel(): string {
    return 'email';
  }
}
 
class SMSNotification implements Notification {
  constructor(private readonly phoneNumber: string) {}
 
  send(message: string): void {
    console.log(`📱 SMS to ${this.phoneNumber}: ${message}`);
  }
 
  getChannel(): string {
    return 'sms';
  }
}
 
class PushNotification implements Notification {
  constructor(private readonly deviceToken: string) {}
 
  send(message: string): void {
    console.log(`🔔 Push to ${this.deviceToken}: ${message}`);
  }
 
  getChannel(): string {
    return 'push';
  }
}
 
// Creator (abstract)
abstract class NotificationCreator {
  // Factory Method — subclasses decide what to create
  abstract createNotification(recipient: string): Notification;
 
  // Template method using the factory method
  notify(recipient: string, message: string): void {
    const notification = this.createNotification(recipient);
    console.log(`Sending via ${notification.getChannel()}...`);
    notification.send(message);
  }
}
 
// Concrete creators
class EmailNotificationCreator extends NotificationCreator {
  createNotification(recipient: string): Notification {
    return new EmailNotification(recipient);
  }
}
 
class SMSNotificationCreator extends NotificationCreator {
  createNotification(recipient: string): Notification {
    return new SMSNotification(recipient);
  }
}
 
class PushNotificationCreator extends NotificationCreator {
  createNotification(recipient: string): Notification {
    return new PushNotification(recipient);
  }
}
 
// Usage
const emailCreator = new EmailNotificationCreator();
emailCreator.notify('user@example.com', 'Your order has shipped!');
// Sending via email...
// 📧 Email to user@example.com: Your order has shipped!
 
const smsCreator = new SMSNotificationCreator();
smsCreator.notify('+1234567890', 'Your order has shipped!');
// Sending via sms...
// 📱 SMS to +1234567890: Your order has shipped!

Simple Factory (Not a GoF Pattern, But Common)

Before covering Abstract Factory, note that many codebases use a Simple Factory — a static method or class that centralizes creation without the inheritance hierarchy. It's simpler but less flexible:

// Simple Factory — not a GoF pattern, but pragmatic
class NotificationFactory {
  static create(
    type: 'email' | 'sms' | 'push',
    recipient: string
  ): Notification {
    switch (type) {
      case 'email':
        return new EmailNotification(recipient);
      case 'sms':
        return new SMSNotification(recipient);
      case 'push':
        return new PushNotification(recipient);
      default:
        throw new Error(`Unknown notification type: ${type}`);
    }
  }
}
 
// Usage — clean, centralized, but still tightly coupled to concrete types
const notification = NotificationFactory.create('email', 'user@example.com');
notification.send('Hello!');

When to prefer Simple Factory over Factory Method:

  • Small, stable set of types (not growing much)
  • No need to extend via subclassing
  • Prototyping or smaller applications

Python Implementation

from abc import ABC, abstractmethod
from typing import Protocol
 
 
class Notification(Protocol):
    def send(self, message: str) -> None: ...
    def get_channel(self) -> str: ...
 
 
class EmailNotification:
    def __init__(self, recipient: str) -> None:
        self.recipient = recipient
 
    def send(self, message: str) -> None:
        print(f"📧 Email to {self.recipient}: {message}")
 
    def get_channel(self) -> str:
        return "email"
 
 
class SMSNotification:
    def __init__(self, phone_number: str) -> None:
        self.phone_number = phone_number
 
    def send(self, message: str) -> None:
        print(f"📱 SMS to {self.phone_number}: {message}")
 
    def get_channel(self) -> str:
        return "sms"
 
 
class NotificationCreator(ABC):
    @abstractmethod
    def create_notification(self, recipient: str) -> Notification:
        pass
 
    def notify(self, recipient: str, message: str) -> None:
        notification = self.create_notification(recipient)
        print(f"Sending via {notification.get_channel()}...")
        notification.send(message)
 
 
class EmailNotificationCreator(NotificationCreator):
    def create_notification(self, recipient: str) -> Notification:
        return EmailNotification(recipient)
 
 
class SMSNotificationCreator(NotificationCreator):
    def create_notification(self, recipient: str) -> Notification:
        return SMSNotification(recipient)
 
 
# Usage
creator = EmailNotificationCreator()
creator.notify("user@example.com", "Your order has shipped!")
# Sending via email...
# 📧 Email to user@example.com: Your order has shipped!

Java Implementation

// Product interface
public interface Notification {
    void send(String message);
    String getChannel();
}
 
// Concrete products
public class EmailNotification implements Notification {
    private final String recipient;
 
    public EmailNotification(String recipient) {
        this.recipient = recipient;
    }
 
    @Override
    public void send(String message) {
        System.out.println("📧 Email to " + recipient + ": " + message);
    }
 
    @Override
    public String getChannel() { return "email"; }
}
 
public class SMSNotification implements Notification {
    private final String phoneNumber;
 
    public SMSNotification(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }
 
    @Override
    public void send(String message) {
        System.out.println("📱 SMS to " + phoneNumber + ": " + message);
    }
 
    @Override
    public String getChannel() { return "sms"; }
}
 
// Creator (abstract class with factory method)
public abstract class NotificationCreator {
    // Factory Method — subclasses override this
    public abstract Notification createNotification(String recipient);
 
    // Template method
    public void notify(String recipient, String message) {
        Notification notification = createNotification(recipient);
        System.out.println("Sending via " + notification.getChannel() + "...");
        notification.send(message);
    }
}
 
// Concrete creators
public class EmailNotificationCreator extends NotificationCreator {
    @Override
    public Notification createNotification(String recipient) {
        return new EmailNotification(recipient);
    }
}
 
public class SMSNotificationCreator extends NotificationCreator {
    @Override
    public Notification createNotification(String recipient) {
        return new SMSNotification(recipient);
    }
}
 
// Usage
NotificationCreator creator = new EmailNotificationCreator();
creator.notify("user@example.com", "Your order has shipped!");

Part 2: Abstract Factory Pattern

Intent

Provide an interface for creating families of related objects without specifying their concrete classes. Think of it as a "factory of factories" — each concrete factory produces a complete, consistent set of related products.

When to Use Abstract Factory vs Factory Method

Factory MethodAbstract Factory
CreatesOne product typeMultiple related product types
MechanismInheritance (subclass overrides)Composition (inject factory interface)
Use whenOne variation pointMultiple consistent variation points
ExampleDifferent notification channelsCross-platform UI (buttons + checkboxes + inputs)

Classic Example: Cross-Platform UI

TypeScript Implementation

// Abstract products
interface Button {
  render(): void;
  onClick(handler: () => void): void;
}
 
interface Checkbox {
  render(): void;
  toggle(): void;
  isChecked(): boolean;
}
 
interface TextInput {
  render(): void;
  getValue(): string;
  setValue(value: string): void;
}
 
// Concrete products — Windows family
class WindowsButton implements Button {
  private handler?: () => void;
 
  render(): void {
    console.log('[Windows] Rendering button with Win32 style');
  }
 
  onClick(handler: () => void): void {
    this.handler = handler;
  }
}
 
class WindowsCheckbox implements Checkbox {
  private checked = false;
 
  render(): void {
    console.log(`[Windows] Rendering checkbox (${this.checked ? '✓' : '○'})`);
  }
 
  toggle(): void {
    this.checked = !this.checked;
  }
 
  isChecked(): boolean {
    return this.checked;
  }
}
 
class WindowsTextInput implements TextInput {
  private value = '';
 
  render(): void {
    console.log(`[Windows] Rendering text input: "${this.value}"`);
  }
 
  getValue(): string {
    return this.value;
  }
 
  setValue(value: string): void {
    this.value = value;
  }
}
 
// Concrete products — Mac family
class MacButton implements Button {
  private handler?: () => void;
 
  render(): void {
    console.log('[Mac] Rendering button with macOS style');
  }
 
  onClick(handler: () => void): void {
    this.handler = handler;
  }
}
 
class MacCheckbox implements Checkbox {
  private checked = false;
 
  render(): void {
    console.log(`[Mac] Rendering checkbox (${this.checked ? '●' : '○'})`);
  }
 
  toggle(): void {
    this.checked = !this.checked;
  }
 
  isChecked(): boolean {
    return this.checked;
  }
}
 
class MacTextInput implements TextInput {
  private value = '';
 
  render(): void {
    console.log(`[Mac] Rendering text input: "${this.value}"`);
  }
 
  getValue(): string {
    return this.value;
  }
 
  setValue(value: string): void {
    this.value = value;
  }
}
 
// Abstract Factory interface
interface UIFactory {
  createButton(): Button;
  createCheckbox(): Checkbox;
  createTextInput(): TextInput;
}
 
// Concrete factories — each produces a consistent FAMILY
class WindowsUIFactory implements UIFactory {
  createButton(): Button {
    return new WindowsButton();
  }
 
  createCheckbox(): Checkbox {
    return new WindowsCheckbox();
  }
 
  createTextInput(): TextInput {
    return new WindowsTextInput();
  }
}
 
class MacUIFactory implements UIFactory {
  createButton(): Button {
    return new MacButton();
  }
 
  createCheckbox(): Checkbox {
    return new MacCheckbox();
  }
 
  createTextInput(): TextInput {
    return new MacTextInput();
  }
}
 
// Application — depends only on the abstract factory
class LoginForm {
  private button: Button;
  private usernameInput: TextInput;
  private rememberMe: Checkbox;
 
  constructor(factory: UIFactory) {
    this.button = factory.createButton();
    this.usernameInput = factory.createTextInput();
    this.rememberMe = factory.createCheckbox();
  }
 
  render(): void {
    this.usernameInput.render();
    this.rememberMe.render();
    this.button.render();
  }
}
 
// Usage — inject the right factory at startup
function createFactory(): UIFactory {
  const os = process.platform;
  if (os === 'win32') return new WindowsUIFactory();
  if (os === 'darwin') return new MacUIFactory();
  return new MacUIFactory(); // default
}
 
const factory = createFactory();
const loginForm = new LoginForm(factory);
loginForm.render();
// [Mac] Rendering text input: ""
// [Mac] Rendering checkbox (○)
// [Mac] Rendering button with macOS style

Python Implementation

from abc import ABC, abstractmethod
import sys
 
 
class Button(ABC):
    @abstractmethod
    def render(self) -> None: ...
 
    @abstractmethod
    def on_click(self, handler) -> None: ...
 
 
class Checkbox(ABC):
    @abstractmethod
    def render(self) -> None: ...
 
    @abstractmethod
    def toggle(self) -> None: ...
 
    @abstractmethod
    def is_checked(self) -> bool: ...
 
 
class TextInput(ABC):
    @abstractmethod
    def render(self) -> None: ...
 
    @abstractmethod
    def get_value(self) -> str: ...
 
    @abstractmethod
    def set_value(self, value: str) -> None: ...
 
 
# Windows family
class WindowsButton(Button):
    def render(self) -> None:
        print("[Windows] Rendering button with Win32 style")
 
    def on_click(self, handler) -> None:
        self._handler = handler
 
 
class WindowsCheckbox(Checkbox):
    def __init__(self):
        self._checked = False
 
    def render(self) -> None:
        symbol = "✓" if self._checked else "○"
        print(f"[Windows] Rendering checkbox ({symbol})")
 
    def toggle(self) -> None:
        self._checked = not self._checked
 
    def is_checked(self) -> bool:
        return self._checked
 
 
class WindowsTextInput(TextInput):
    def __init__(self):
        self._value = ""
 
    def render(self) -> None:
        print(f'[Windows] Rendering text input: "{self._value}"')
 
    def get_value(self) -> str:
        return self._value
 
    def set_value(self, value: str) -> None:
        self._value = value
 
 
# Mac family
class MacButton(Button):
    def render(self) -> None:
        print("[Mac] Rendering button with macOS style")
 
    def on_click(self, handler) -> None:
        self._handler = handler
 
 
class MacCheckbox(Checkbox):
    def __init__(self):
        self._checked = False
 
    def render(self) -> None:
        symbol = "●" if self._checked else "○"
        print(f"[Mac] Rendering checkbox ({symbol})")
 
    def toggle(self) -> None:
        self._checked = not self._checked
 
    def is_checked(self) -> bool:
        return self._checked
 
 
class MacTextInput(TextInput):
    def __init__(self):
        self._value = ""
 
    def render(self) -> None:
        print(f'[Mac] Rendering text input: "{self._value}"')
 
    def get_value(self) -> str:
        return self._value
 
    def set_value(self, value: str) -> None:
        self._value = value
 
 
# Abstract Factory
class UIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button: ...
 
    @abstractmethod
    def create_checkbox(self) -> Checkbox: ...
 
    @abstractmethod
    def create_text_input(self) -> TextInput: ...
 
 
class WindowsUIFactory(UIFactory):
    def create_button(self) -> Button:
        return WindowsButton()
 
    def create_checkbox(self) -> Checkbox:
        return WindowsCheckbox()
 
    def create_text_input(self) -> TextInput:
        return WindowsTextInput()
 
 
class MacUIFactory(UIFactory):
    def create_button(self) -> Button:
        return MacButton()
 
    def create_checkbox(self) -> Checkbox:
        return MacCheckbox()
 
    def create_text_input(self) -> TextInput:
        return MacTextInput()
 
 
class LoginForm:
    def __init__(self, factory: UIFactory) -> None:
        self.button = factory.create_button()
        self.username_input = factory.create_text_input()
        self.remember_me = factory.create_checkbox()
 
    def render(self) -> None:
        self.username_input.render()
        self.remember_me.render()
        self.button.render()
 
 
# Usage
factory: UIFactory
if sys.platform == "win32":
    factory = WindowsUIFactory()
else:
    factory = MacUIFactory()
 
login_form = LoginForm(factory)
login_form.render()

Java Implementation

// Abstract products
public interface Button {
    void render();
    void onClick(Runnable handler);
}
 
public interface Checkbox {
    void render();
    void toggle();
    boolean isChecked();
}
 
public interface TextInput {
    void render();
    String getValue();
    void setValue(String value);
}
 
// Windows family
public class WindowsButton implements Button {
    @Override
    public void render() {
        System.out.println("[Windows] Rendering button with Win32 style");
    }
 
    @Override
    public void onClick(Runnable handler) { /* register handler */ }
}
 
public class WindowsCheckbox implements Checkbox {
    private boolean checked = false;
 
    @Override
    public void render() {
        System.out.println("[Windows] Rendering checkbox (" + (checked ? "✓" : "○") + ")");
    }
 
    @Override
    public void toggle() { checked = !checked; }
 
    @Override
    public boolean isChecked() { return checked; }
}
 
// Abstract Factory interface
public interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
    TextInput createTextInput();
}
 
// Concrete factories
public class WindowsUIFactory implements UIFactory {
    @Override
    public Button createButton() { return new WindowsButton(); }
 
    @Override
    public Checkbox createCheckbox() { return new WindowsCheckbox(); }
 
    @Override
    public TextInput createTextInput() { return new WindowsTextInput(); }
}
 
public class MacUIFactory implements UIFactory {
    @Override
    public Button createButton() { return new MacButton(); }
 
    @Override
    public Checkbox createCheckbox() { return new MacCheckbox(); }
 
    @Override
    public TextInput createTextInput() { return new MacTextInput(); }
}
 
// Application — only depends on the interface
public class LoginForm {
    private final Button button;
    private final TextInput usernameInput;
    private final Checkbox rememberMe;
 
    public LoginForm(UIFactory factory) {
        this.button = factory.createButton();
        this.usernameInput = factory.createTextInput();
        this.rememberMe = factory.createCheckbox();
    }
 
    public void render() {
        usernameInput.render();
        rememberMe.render();
        button.render();
    }
}
 
// Usage
UIFactory factory = getFactoryForOS(); // returns Windows or Mac factory
LoginForm form = new LoginForm(factory);
form.render();

Real-World Use Case: Database Access Layer

Abstract Factory is ideal for abstracting database-specific implementations:

// Abstract products
interface DatabaseConnection {
  query<T>(sql: string, params?: unknown[]): Promise<T[]>;
  close(): Promise<void>;
}
 
interface DatabaseTransaction {
  begin(): Promise<void>;
  commit(): Promise<void>;
  rollback(): Promise<void>;
  query<T>(sql: string, params?: unknown[]): Promise<T[]>;
}
 
interface DatabaseConnectionPool {
  acquire(): Promise<DatabaseConnection>;
  release(conn: DatabaseConnection): void;
  drain(): Promise<void>;
}
 
// Abstract factory
interface DatabaseFactory {
  createConnection(config: DatabaseConfig): DatabaseConnection;
  createTransaction(conn: DatabaseConnection): DatabaseTransaction;
  createPool(config: PoolConfig): DatabaseConnectionPool;
}
 
// Concrete factories
class PostgreSQLFactory implements DatabaseFactory {
  createConnection(config: DatabaseConfig): DatabaseConnection {
    return new PostgreSQLConnection(config);
  }
 
  createTransaction(conn: DatabaseConnection): DatabaseTransaction {
    return new PostgreSQLTransaction(conn as PostgreSQLConnection);
  }
 
  createPool(config: PoolConfig): DatabaseConnectionPool {
    return new PostgreSQLPool(config);
  }
}
 
class SQLiteFactory implements DatabaseFactory {
  createConnection(config: DatabaseConfig): DatabaseConnection {
    return new SQLiteConnection(config);
  }
 
  createTransaction(conn: DatabaseConnection): DatabaseTransaction {
    return new SQLiteTransaction(conn as SQLiteConnection);
  }
 
  createPool(config: PoolConfig): DatabaseConnectionPool {
    return new SQLitePool(config);  // SQLite handles pooling differently
  }
}
 
// Repository only depends on the abstract interfaces
class UserRepository {
  constructor(
    private readonly factory: DatabaseFactory,
    private readonly config: DatabaseConfig
  ) {}
 
  async findById(id: number): Promise<User | null> {
    const conn = this.factory.createConnection(this.config);
    try {
      const rows = await conn.query<User>(
        'SELECT * FROM users WHERE id = $1', [id]
      );
      return rows[0] ?? null;
    } finally {
      await conn.close();
    }
  }
 
  async createWithTransaction(userData: CreateUserInput): Promise<User> {
    const conn = this.factory.createConnection(this.config);
    const tx = this.factory.createTransaction(conn);
    await tx.begin();
    try {
      const [user] = await tx.query<User>(
        'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
        [userData.name, userData.email]
      );
      await tx.commit();
      return user;
    } catch (error) {
      await tx.rollback();
      throw error;
    } finally {
      await conn.close();
    }
  }
}
 
// Usage — switch databases by swapping the factory
const factory = process.env.NODE_ENV === 'test'
  ? new SQLiteFactory()      // Fast in-memory for tests
  : new PostgreSQLFactory();  // Production database
 
const userRepo = new UserRepository(factory, dbConfig);

Real-World Use Case: Parser System

Factory Method is a natural fit for parser families:

interface Document {
  getContent(): string;
  getMetadata(): Record<string, string>;
}
 
interface DocumentParser {
  parse(input: string): Document;
}
 
// Factory Method pattern — each subclass handles a format
abstract class DocumentProcessor {
  // Factory method
  protected abstract createParser(): DocumentParser;
 
  // Template method using the factory method
  process(input: string): ProcessedDocument {
    const parser = this.createParser();
    const doc = parser.parse(input);
    return this.transform(doc);
  }
 
  private transform(doc: Document): ProcessedDocument {
    return {
      content: doc.getContent().trim(),
      metadata: doc.getMetadata(),
      processedAt: new Date(),
    };
  }
}
 
class JSONDocumentProcessor extends DocumentProcessor {
  protected createParser(): DocumentParser {
    return new JSONParser();
  }
}
 
class XMLDocumentProcessor extends DocumentProcessor {
  protected createParser(): DocumentParser {
    return new XMLParser();
  }
}
 
class MarkdownDocumentProcessor extends DocumentProcessor {
  protected createParser(): DocumentParser {
    return new MarkdownParser();
  }
}
 
// Registry pattern — pick the right processor at runtime
const processorRegistry: Record<string, DocumentProcessor> = {
  json: new JSONDocumentProcessor(),
  xml: new XMLDocumentProcessor(),
  md: new MarkdownDocumentProcessor(),
};
 
function processDocument(format: string, input: string): ProcessedDocument {
  const processor = processorRegistry[format];
  if (!processor) throw new Error(`Unsupported format: ${format}`);
  return processor.process(input);
}

Testing with Factories

One major benefit of factories: easy to test with mocks.

// Test double factory — returns predictable objects
class MockUIFactory implements UIFactory {
  createButton(): Button {
    return {
      render: jest.fn(),
      onClick: jest.fn(),
    };
  }
 
  createCheckbox(): Checkbox {
    return {
      render: jest.fn(),
      toggle: jest.fn(),
      isChecked: jest.fn().mockReturnValue(false),
    };
  }
 
  createTextInput(): TextInput {
    return {
      render: jest.fn(),
      getValue: jest.fn().mockReturnValue(''),
      setValue: jest.fn(),
    };
  }
}
 
// Test
describe('LoginForm', () => {
  it('renders all components on initialization', () => {
    const mockFactory = new MockUIFactory();
    const form = new LoginForm(mockFactory);
 
    form.render();
 
    // Verify each component was asked to render
    // (cast to get access to jest mock methods)
    const button = mockFactory.createButton();
    expect(button.render).toHaveBeenCalled();
  });
});
 
// Similarly for the database example:
class MockDatabaseFactory implements DatabaseFactory {
  createConnection(): DatabaseConnection {
    return {
      query: jest.fn().mockResolvedValue([]),
      close: jest.fn().mockResolvedValue(undefined),
    };
  }
 
  createTransaction(conn: DatabaseConnection): DatabaseTransaction {
    return {
      begin: jest.fn().mockResolvedValue(undefined),
      commit: jest.fn().mockResolvedValue(undefined),
      rollback: jest.fn().mockResolvedValue(undefined),
      query: jest.fn().mockResolvedValue([]),
    };
  }
 
  createPool(): DatabaseConnectionPool {
    return {
      acquire: jest.fn(),
      release: jest.fn(),
      drain: jest.fn().mockResolvedValue(undefined),
    };
  }
}
 
describe('UserRepository', () => {
  it('returns null when user not found', async () => {
    const factory = new MockDatabaseFactory();
    const repo = new UserRepository(factory, testConfig);
 
    const user = await repo.findById(999);
 
    expect(user).toBeNull();
  });
});

When to Use Each Pattern

Summary

ScenarioPattern to Use
Create one product, subclasses decide whichFactory Method
Create a family of consistent productsAbstract Factory
Small, fixed set of types, no subclassingSimple Factory
Need to swap entire product families (OS, DB, theme)Abstract Factory
Framework expects you to override a creation methodFactory Method

Common Pitfalls

1. Over-engineering with Abstract Factory

// ❌ Overkill — Abstract Factory for a single product type
interface ButtonFactory {
  createButton(): Button;  // Only one product? Use Factory Method instead
}
 
// ✅ Abstract Factory is for families of products
interface UIFactory {
  createButton(): Button;
  createCheckbox(): Checkbox;   // Multiple related products
  createTextInput(): TextInput; // that need to stay consistent
}

2. Factories that know too much

// ❌ Factory with business logic — violates Single Responsibility
class UserFactory {
  static create(data: UserInput): User {
    const user = new User(data);
    user.sendWelcomeEmail();  // ❌ Not the factory's job
    database.save(user);       // ❌ Not the factory's job
    auditLog.record(user);     // ❌ Not the factory's job
    return user;
  }
}
 
// ✅ Factory only creates
class UserFactory {
  static create(data: UserInput): User {
    return new User(data);
  }
}

3. Forgetting to register new types

// ❌ Registry-based factory that doesn't error loudly
class ParserFactory {
  private static parsers: Map<string, DocumentParser> = new Map([
    ['json', new JSONParser()],
    ['xml', new XMLParser()],
    // New developer adds MarkdownParser but forgets to register here
  ]);
 
  static create(format: string): DocumentParser {
    return this.parsers.get(format) ?? new JSONParser(); // ❌ Silent fallback
  }
}
 
// ✅ Fail loudly
static create(format: string): DocumentParser {
  const parser = this.parsers.get(format);
  if (!parser) {
    throw new Error(
      `No parser registered for format: "${format}". ` +
      `Registered formats: ${[...this.parsers.keys()].join(', ')}`
    );
  }
  return parser;
}

Summary and Key Takeaways

Factory Method:
✅ Centralizes creation of one product type
✅ Lets subclasses decide what concrete class to instantiate
✅ Great for frameworks where users override creation hooks
✅ Follows Open/Closed Principle — add new products without modifying existing creators

Abstract Factory:
✅ Creates families of related products that must be consistent
✅ The client depends only on abstract interfaces — never on concrete classes
✅ Perfect for cross-platform, multi-database, or multi-theme systems
✅ Makes it trivial to swap entire families (e.g., Windows → Mac UI)

What's Next

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.