Mail

Factory Method Design Pattern in TypeScript: A Beginner’s Guide

26 Aug 2025

Table of contents

Prerequisites

These are the basic prerequisites for this blog. If you’re not familiar with first two points. this blog might not be suitable for you. Otherwise, you’re good to go! The examples are really simple and can be easily replicated in other languages.

Introduction

Hey devs, Finally i came up with my first blog on design patterns. Today we are going to learn factory method pattern. Yeah, you heard me right As I mentioned in importance of design patterns we are going to learn it together. In this entire design pattern series i will be using an application called Ticket booking system. Here, at the center we are having Booking, which connects Customer with a Resource for a specific time slot. Each booking has an ID, and the ID of the resource, the ID of the customer, the time slot chosen and a status. The Resource could be anything that can be reserved - a room in a hotel, a seat in theatre, or even a vehicle. The Customer is the person making the booking. To support the process, there’s a Payment part that handles money for the booking, an AvailabilityService that checks if a resource is free at a given time, and a NotificationService that sends updates or reminders to customers. Put together, these pieces form a compact system where customers can book resources, pay for them, and get notified about their reservations. We will use this application to decide which problem needs which specific pattern and also where we dont want to use a design pattern. I think that’s enough for the introduction let’s get into little bit of theory.

What is Factory Method Pattern

Factory Method

Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

Problem

Imagine in our booking platform. In the first version, the app only supports payments through Stripe, so most of the payment-related code is directly tied to the StripeProcessor class. Over time, the platform grows and stakeholders decided to include PayPal support. It sounds good right, but here’s the catch: since the code is tightly coupled to StripeProcessor, adding PayPal means we’ll have to touch multiple parts of the codebase. And if later if we want to add even more options like Apple Pay or Google Pay, the same problem repeats.

Without a good design, the code quickly becomes messy, full of conditionals that check “which payment type is this?” before deciding how to handle it.

Solution

This is where the Factory Method comes in. Instead of directly creating payment processors all over the place, we delegate that responsibility to a PaymentFactory with a factory method PaymentFactory.createPaymentProcessor(), now this method will return a payment type StripeProcessor, PaypalProcessor, or any future payment type. The client code will use the instance without conditionals and switch statements.

Factory Method Diagram

FactoryMethod.png

Before understanding the solution we have to learn some terms. Let’s start with Product it is an interface which will be returned by the creator and its subclasses. Then, the Concrete Products these are different implementations of the product interface. Next the Creator class which declares the factory method that returns new product objects. It’s important that the return type of this method matches the product interface. Next, the Concrete Creator classes these will override the base factory method. Finally the Client Code it is the code that uses the Creator and Products without depending on their concrete classes. Now look at the solution we created using the factory method pattern

Payment Factory

PaymentFactory.png

The Factory Method pattern suggests that instead of creating payment processors directly in the client code (using new StripeProcessor() or new PayPalProcessor()), we delegate that responsibility to a dedicated factory method. The objects are still created with the new operator under the hood, but this call is now wrapped inside the factory. The objects returned by the Creator’s Factory method are often referred to as Products.

At first, this may look like an unnecessary change. After all, we’re just moving the new call from one place to another. But here we are centralizing object creation in a factory, we can easily switch or extend the payment processors without touching the rest of the booking payment logic.

The only requirement is that all payment processors implement a common interface, in this case PaymentProcessor, which declares a method callPaymentApi(). Both StripeProcessor and PayPalProcessor implement this interface, but each one knows how to talk to its own payment gateway.

The PaymentFactory defines the factory method createPaymentProcessor(). Subclasses such as StripePaymentFactory and PayPalPaymentFactory override this method and decide which concrete processor to return. The client code that triggers a payment doesn’t need to know if it’s dealing with Stripe, PayPal, or some new processor we add in the future. It just asks the factory for a PaymentProcessor and calls callPaymentApi(). Thanks to the shared interface, the client doesn’t need to worry about the details of each implementation it just knows that the payment will be processed.

This way, the booking platform stays clean and flexible. Adding support for a new payment provider no longer requires touching the booking flow. You only need to add a new processor class and extend the factory the rest of the system remains unchanged.

Basic Implementation

PaymentProcessors.ts

interface PaymentProcessor {
  // Sample method to call the payment api
  callPaymentApi(amount: number): void;
}

class StripeProcessor implements PaymentProcessor {
  callPaymentApi(amount: number) {
    console.log(`Processing ${amount} with Stripe`);
  }
}

class PayPalProcessor implements PaymentProcessor {
  callPaymentApi(amount: number) {
    console.log(`Processing ${amount} with PayPal`);
  }
}

FactoryCreators.ts

abstract class PaymentFactory {
  // Factory method
  abstract createPaymentProcessor(): PaymentProcessor;

  // Other business logics related to payment
}

class StripePaymentFactory extends PaymentFactory {
  createPaymentProcessor(): PaymentProcessor {
    return new StripeProcessor();
  }
}

class PayPalPaymentFactory extends PaymentFactory {
  createPaymentProcessor(): PaymentProcessor {
    return new PayPalProcessor();
  }
}

// Future payment processors

Application.ts

export class Application {
  private factory: PaymentFactory;

  constructor(config: string) {
    if (config === "stripe") {
      this.factory = new StripePaymentFactory();
    } else if (config === "paypal") {
      this.factory = new PayPalPaymentFactory();
    } else {
      throw new Error("Unsupported payment provider");
    }
  }

  run(amount: number) {
    const processor = this.factory.createPaymentProcessor();
    processor.callPaymentApi(amount);
  }
}

index.ts

import { Application } from "./Application";

const CONFIG = "paypal"; // In a real-world scenario, this would come from a config file, env var, or user input

const app = new Application(CONFIG);
app.run(500);

Bad code vs Factory method pattern

BadCode.ts

// Concrete payment processors
class StripeProcessor {
  callPaymentApi(amount: number) {
    console.log(`Processing ${amount} with Stripe`);
  }
}

class PayPalProcessor {
  callPaymentApi(amount: number) {
    console.log(`Processing ${amount} with PayPal`);
  }
}

// client code
function makePayment(type: string, amount: number) {
  if (type === "stripe") {
    const processor = new StripeProcessor();
    processor.callPaymentApi(amount);
  } else if (type === "paypal") {
    const processor = new PayPalProcessor();
    processor.callPaymentApi(amount);
  } else {
    throw new Error("Unsupported payment type");
  }
}

// Usage
makePayment("stripe", 100);
makePayment("paypal", 50);

The problem with this approach is that the booking logic ends up bloated with lot of if/else checks. Every time we want to add a new payment option, like Apple Pay, we have to come back and hack more conditions into the function. On top of that, the client code knows way too much about the actual payment processors like Stripe, PayPal, whatever. It feels messy and tightly coupled, which is exactly the kind of thing that makes code harder to maintain in the long run.

On the other hand the factory method pattern setup have a super clean client code setup. No messy if/else branches. If we ever need to support a new payment option, we just create a new PaymentProcessor and its factory. That’s it we don’t have to touch the existing booking flow at all. And since the booking logic only talks to the PaymentProcessor interface, it doesn’t care whether the processor is Stripe, PayPal, or something brand new. The code stays flexible, decoupled, and way easier to maintain.

When to use it

When not to use it

Real world use cases

  1. I’ve personally used the Factory Method pattern in one of the business flows I worked on. The requirement was to handle multiple types of user subscriptions. Each subscription type triggered a different service API call, and I implemented this using the Factory Method. Instead of writing a bunch of if/else checks, the factory neatly created the right service handler depending on the subscription.
  2. You might have seen a similar pattern in the Bot Framework SDK, especially when it comes to managing user and conversation state storage. For example, UserState and ConversationState act like the products, while classes like MemoryStorage, UserMemory, and ConversationMemory act as the creators. The createStorage method is the factory method. The SDK actually mixes in a few related patterns as well. It might have lot of use cases but I explored only these two as of now.

Conclusion

Ok devs, that’s it for today. I tried my best to explain this pattern. if you have any queries are suggestions please feel free to reach me out.

Code, learn, refactor, repeat

Reference

Stay Connected

GitHub LinkedIn X Daily.dev Email

© 2025 Chiristo. Feel free to share or reference this post with proper credit