Factory Pattern

The Factory pattern provides an interface for creating objects without specifying their exact classes. This pattern is particularly valuable in legacy systems for managing object creation complexity, enabling dependency injection, and making systems more testable and flexible.

Video Summary

The video presentation explores the Factory pattern:

  • What factories are and why they're useful
  • Different types: Simple Factory, Factory Method, Abstract Factory
  • Using factories to manage object creation complexity
  • Enabling dependency injection and testability
  • Real-world examples in legacy modernization
  • When to use factories vs. direct instantiation

Key Concepts

1. Encapsulating Object Creation

Instead of scattering new keywords throughout code, centralize creation logic:

// Without factory: creation logic scattered
const user = new User(data);
const premium = new PremiumUser(data);
const admin = new AdminUser(data);

// With factory: centralized creation
const user = UserFactory.create(data);

2. Types of Factories

Simple Factory: Single method creates objects

class UserFactory {
  static create(type: string, data: UserData): User {
    switch (type) {
      case 'admin':
        return new AdminUser(data);
      case 'premium':
        return new PremiumUser(data);
      default:
        return new RegularUser(data);
    }
  }
}

Factory Method: Subclasses decide which class to instantiate

abstract class UserRepository {
  abstract createUser(): User;

  saveUser(data: UserData): User {
    const user = this.createUser();
    user.populate(data);
    return user;
  }
}

class AdminRepository extends UserRepository {
  createUser(): User {
    return new AdminUser();
  }
}

class RegularRepository extends UserRepository {
  createUser(): User {
    return new RegularUser();
  }
}

Abstract Factory: Creates families of related objects

interface DatabaseFactory {
  createConnection(): Connection;
  createQueryBuilder(): QueryBuilder;
  createTransaction(): Transaction;
}

class MySQLFactory implements DatabaseFactory {
  createConnection(): Connection {
    return new MySQLConnection();
  }

  createQueryBuilder(): QueryBuilder {
    return new MySQLQueryBuilder();
  }

  createTransaction(): Transaction {
    return new MySQLTransaction();
  }
}

class PostgreSQLFactory implements DatabaseFactory {
  createConnection(): Connection {
    return new PostgreSQLConnection();
  }

  createQueryBuilder(): QueryBuilder {
    return new PostgreSQLQueryBuilder();
  }

  createTransaction(): Transaction {
    return new PostgreSQLTransaction();
  }
}

Real-World Applications

Example 1: Payment Processor Factory

interface PaymentProcessor {
  processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult>;
}

class StripeProcessor implements PaymentProcessor {
  async processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult> {
    // Stripe-specific logic
  }
}

class PayPalProcessor implements PaymentProcessor {
  async processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult> {
    // PayPal-specific logic
  }
}

class PaymentProcessorFactory {
  static create(method: string): PaymentProcessor {
    switch (method) {
      case 'stripe':
        return new StripeProcessor();
      case 'paypal':
        return new PayPalProcessor();
      default:
        throw new Error(`Unknown payment method: ${method}`);
    }
  }
}

// Usage
const processor = PaymentProcessorFactory.create(order.paymentMethod);
await processor.processPayment(order.total, order.paymentDetails);

Example 2: Database Connection Factory

class DatabaseFactory {
  static createConnection(config: DatabaseConfig): Database {
    switch (config.type) {
      case 'mysql':
        return new MySQLDatabase(config);
      case 'postgres':
        return new PostgresDatabase(config);
      case 'mongodb':
        return new MongoDatabase(config);
      default:
        throw new Error(`Unsupported database: ${config.type}`);
    }
  }
}

// Configuration-driven
const db = DatabaseFactory.createConnection({
  type: process.env.DB_TYPE,
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT),
});

Example 3: Report Generator Factory

interface ReportGenerator {
  generate(data: any): string;
}

class PDFReportGenerator implements ReportGenerator {
  generate(data: any): string {
    // Generate PDF
    return 'pdf-content';
  }
}

class ExcelReportGenerator implements ReportGenerator {
  generate(data: any): string {
    // Generate Excel
    return 'excel-content';
  }
}

class CSVReportGenerator implements ReportGenerator {
  generate(data: any): string {
    // Generate CSV
    return 'csv-content';
  }
}

class ReportFactory {
  static create(format: string): ReportGenerator {
    const generators = {
      pdf: PDFReportGenerator,
      excel: ExcelReportGenerator,
      csv: CSVReportGenerator,
    };

    const GeneratorClass = generators[format];

    if (!GeneratorClass) {
      throw new Error(`Unknown report format: ${format}`);
    }

    return new GeneratorClass();
  }
}

// Usage
app.get('/reports/:format', async (req, res) => {
  const generator = ReportFactory.create(req.params.format);
  const report = generator.generate(await fetchData());

  res.send(report);
});

Example 4: Notification Service Factory

interface NotificationService {
  send(to: string, message: string): Promise<void>;
}

class EmailNotification implements NotificationService {
  async send(to: string, message: string): Promise<void> {
    // Send email
  }
}

class SMSNotification implements NotificationService {
  async send(to: string, message: string): Promise<void> {
    // Send SMS
  }
}

class PushNotification implements NotificationService {
  async send(to: string, message: string): Promise<void> {
    // Send push notification
  }
}

class NotificationFactory {
  private static services = new Map<string, NotificationService>();

  static register(type: string, service: NotificationService): void {
    this.services.set(type, service);
  }

  static create(type: string): NotificationService {
    const service = this.services.get(type);

    if (!service) {
      throw new Error(`No notification service registered for: ${type}`);
    }

    return service;
  }
}

// Setup
NotificationFactory.register('email', new EmailNotification());
NotificationFactory.register('sms', new SMSNotification());
NotificationFactory.register('push', new PushNotification());

// Usage
const service = NotificationFactory.create(user.preferredNotificationMethod);
await service.send(user.contact, 'Your order has shipped');

Example 5: Legacy System Adapter Factory

// Abstract interface
interface OrderService {
  createOrder(order: Order): Promise<OrderResult>;
}

// Legacy SOAP service adapter
class LegacySOAPOrderService implements OrderService {
  async createOrder(order: Order): Promise<OrderResult> {
    // Convert to SOAP format, call legacy system
  }
}

// Modern REST service
class ModernRESTOrderService implements OrderService {
  async createOrder(order: Order): Promise<OrderResult> {
    // Call modern REST API
  }
}

class OrderServiceFactory {
  static create(): OrderService {
    // Use feature flag to control which implementation
    if (featureFlags.isEnabled('modern-order-service')) {
      return new ModernRESTOrderService();
    }
    return new LegacySOAPOrderService();
  }
}

// Application code doesn't know which implementation is used
const orderService = OrderServiceFactory.create();
await orderService.createOrder(order);

Testing Benefits

Before: Hard to Test

class OrderProcessor {
  processOrder(order: Order): void {
    // Hard-coded dependency
    const paymentProcessor = new StripeProcessor();
    paymentProcessor.processPayment(order.total, order.paymentDetails);
  }
}

// Can't easily test without hitting Stripe

After: Easy to Test

class OrderProcessor {
  constructor(private processorFactory: PaymentProcessorFactory) {}

  processOrder(order: Order): void {
    const processor = this.processorFactory.create(order.paymentMethod);
    processor.processPayment(order.total, order.paymentDetails);
  }
}

// Testing
class MockProcessorFactory {
  create(method: string): PaymentProcessor {
    return new MockPaymentProcessor();
  }
}

const processor = new OrderProcessor(new MockProcessorFactory());

Common Pitfalls

1. God Factory

Problem: Factory that knows about too many classes.

// Bad: Factory knows everything
class ObjectFactory {
  create(type: string): any {
    // 50 different object types
    switch (type) {
      case 'user': return new User();
      case 'order': return new Order();
      case 'product': return new Product();
      // ... 47 more cases
    }
  }
}

Solution: Multiple focused factories:

class UserFactory { /* creates users */ }
class OrderFactory { /* creates orders */ }
class ProductFactory { /* creates products */ }

2. Premature Abstraction

Problem: Creating factories when direct instantiation is fine.

// Overkill for simple case
class StringFactory {
  static create(value: string): string {
    return new String(value).toString();
  }
}

// Just use:
const str = 'hello';

Rule of thumb: Use factories when:

  • Creation logic is complex
  • Need to switch implementations
  • Want to control instantiation

3. Not Using Dependency Injection

Problem: Factory calls scattered throughout code.

// Everywhere in code
const db = DatabaseFactory.create(config);

Solution: Create once, inject everywhere:

// In main.ts
const db = DatabaseFactory.create(config);
const app = new App(db);

// Inject to classes
class UserService {
  constructor(private db: Database) {}
}

Key Takeaways

  • Factory pattern encapsulates object creation logic
  • Three types: Simple Factory, Factory Method, Abstract Factory
  • Useful for complex creation, switching implementations, testing
  • Enables dependency injection and loose coupling
  • Particularly valuable in legacy systems for migration
  • Don't overuse—simple cases don't need factories
  • Combine with dependency injection for maximum flexibility

Further Reading

Sunsetting - Factory Pattern | Sunsetting Learn