DEV Community

Xavier Carrera Gimbert
Xavier Carrera Gimbert

Posted on

The Factory Method Design Pattern Explained for Humans

Design patterns are often explained in a very complicated way, which makes it hard for casual readers to understand.

So today, I would like to explain to you how I understand the Factory Method Pattern in the simplest way possible—and with a real-world example!

The problem we are trying to solve

Let's assume we are trying to support multiple payment methods in our app. It should be easy but in reality it is very cumbersome to implement. All the methods allow users to do the same operations (pay for an order, get a refund, etc.), but each service does it in a different way. You do not process a payment with Stripe in the same way you do with PayPal, for example.

Also, we want to be able to expand the code in the future without breaking the existing one. Our app may need to support Google Pay in the near future. Who knows!

The solution: The Factory Method

We first define the interface that all our payment methods have to follow. In this case, each payment method has to allow users to process a payment and do a refund. No matter if we use Stripe, PayPal or whatever, users have to be able to perform these two operations.

interface PaymentProcessor {
  processPayment(
    amount: number,
  ): Promise<{ success: boolean; transactionId: string }>;
  refund(transactionId: string): Promise<boolean>;
}
Enter fullscreen mode Exit fullscreen mode

Now we define each processor.

ℹ️ I use the word processor rather than method because in this code context I think it makes more sense.

class StripeProcessor implements PaymentProcessor {
  async processPayment(
    amount: number,
  ): Promise<{ success: boolean; transactionId: string }> {
    console.log(`Processing $${amount} via Stripe...`);
    // Stripe API call
    return { success: true, transactionId: 'stripe_tx_12345' };
  }

  async refund(transactionId: string): Promise<boolean> {
    console.log(`Refunding Stripe transaction ${transactionId}`);
    return true;
  }
}

class PayPalProcessor implements PaymentProcessor {
  async processPayment(
    amount: number,
  ): Promise<{ success: boolean; transactionId: string }> {
    console.log(`Processing $${amount} via PayPal...`);
    // PayPal API call
    return { success: true, transactionId: 'paypal_tx_67890' };
  }

  async refund(transactionId: string): Promise<boolean> {
    console.log(`Refunding PayPal transaction ${transactionId}`);
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

We have to also specify our creator, or in other words, the class that will have the factory method that will instantiate the specific processors and orchestrate their methods.

abstract class PaymentService {
  abstract createProcessor(): PaymentProcessor; // The Factory Method

  async checkout(amount: number): Promise<void> {
    const processor = this.createProcessor();
    const result = await processor.processPayment(amount);

    if (result.success) {
      console.log(
        `Payment successful! Transaction ID: ${result.transactionId}`,
      );
    }
  }

  async refund(transactionId: string): Promise<void> {
    const processor = this.createProcessor();
    const success = await processor.refund(transactionId);

    if (success) {
      console.log(`Refund successful for Transaction ID: ${transactionId}`);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

😨 Are you confused? Don't worry, bear with me; once we finish, you will understand.

Now we create the different implementations of the services by extending the creator class:

class StripePaymentService extends PaymentService {
  createProcessor(): PaymentProcessor {
    return new StripeProcessor();
  }
}

class PayPalPaymentService extends PaymentService {
  createProcessor(): PaymentProcessor {
    return new PayPalProcessor();
  }
}
Enter fullscreen mode Exit fullscreen mode

And finally we apply it in our app:

function processCheckout(paymentMethod: 'stripe' | 'paypal', amount: number) {
  const service =
    paymentMethod === 'stripe'
      ? new StripePaymentService()
      : new PayPalPaymentService();

  service.checkout(amount);
}

processCheckout('stripe', 99.99);
Enter fullscreen mode Exit fullscreen mode

Wait, I do not understand, what happened here?

We have created the classes StripePaymentService and PayPalPaymentService which extend the creator class PaymentService. This creator class uses a factory method createProcessor to create an instance of each the payment processor and use it to orchestrate the checkout and the refund.

Notice that the creator class does not care which payment processor are we using. It simply orchestrates the one it is provided.

Why is this pattern useful?

It allows us to execute different variants (Stripe, PayPal) of a process (processing payments and refunds) with minimal code changes.

We simply instantiate the class we need (StripePaymentService or PayPalPaymentService) and the creator class orchestrates all the different logic.

If in the future we want to add new payment processors it is also very easy to do: we simply create a new processor and extend the creator class (PaymentService).

Where does this pattern shine?

When we have a structure that is repeated multiple times with different implementations.

Like a payment system. The core functionality is the same for all the variants but the implementation changes significantly.

Where is this pattern used?

It is used for example in ORMs that support a lot of database types.

The ORM has the same functionality (query a database, create an item...) for all the databases, but the implementation changes a lot if we use PostgreSQL or MongoDB, for example.

Where can I learn more about this pattern?

Go to one of the most reputable resources to learn about architectural patterns. Refactoring Guru

Good bye!

Hope this article helped you! If you have any doubts, please let me know in the comments.

Top comments (0)