The Problem
An e-commerce platform typically integrates with multiple payment providers (Stripe, PayPal, etc.). Each provider requires its own payment processor and refund handler, and these objects must be compatible with each other — you cannot process a payment through Stripe and then issue a refund through PayPal. If the client code creates these objects directly, it becomes riddled with conditionals, and adding a new payment provider forces changes throughout the codebase.
The Solution
The Abstract Factory pattern defines an interface (PaymentFactory) with methods for creating each member of a product family (createPaymentProcessor() and createRefundHandler()). Each concrete factory (StripeFactory, PaypalFactory) implements this interface and produces objects that are guaranteed to work together. The client code programs against the abstract factory interface and never references concrete classes.
Structure
- AbstractFactory (
PaymentFactory) — Declares the interface for creating each product in the family. - ConcreteFactory (
StripeFactory,PaypalFactory) — Implements the abstract factory interface, producing provider-specific objects. - AbstractProduct (
PaymentProcessor,RefundHandler) — Interfaces that define the contract for each type of product. - ConcreteProduct (
StripePaymentProcessor,StripeRefundHandler,PaypalPaymentProcessor,PaypalRefundHandler) — Provider-specific implementations. - Client (
AbstractFactoryController) — Uses only the abstract factory and abstract product interfaces.
Implementation
This implementation models a payment processing subsystem for an e-commerce checkout. The platform supports two payment providers:
- Stripe — Charges a processing fee of 2.9% + $0.30, generates transaction IDs prefixed with
stripe_txn_, and estimates refunds in 5-10 business days. - PayPal — Charges a processing fee of 3.49% + $0.49, generates transaction IDs prefixed with
paypal_txn_, and estimates refunds in 3-5 business days.
export interface PaymentProcessor {
processPayment(amount: number): {
success: boolean;
transactionId: string;
provider: string;
};
}
export interface RefundHandler {
processRefund(
transactionId: string,
amount: number,
): {
success: boolean;
refundId: string;
provider: string;
};
} To add a new provider (e.g., Square), you would create a SquareFactory, SquarePaymentProcessor, and SquareRefundHandler, register the factory in the module, and add it to the controller’s factory map. No existing code needs to change.
NestJS Integration
In NestJS, each concrete factory is decorated with @Injectable() and registered as a provider in the module. The controller injects both StripeFactory and PaypalFactory and selects the correct one at runtime based on the incoming request.
This pattern maps well to NestJS idioms:
- Custom providers with
useFactory: You could use NestJS’suseFactoryto dynamically select and return the correct factory based on environment configuration. - Module-level encapsulation: Each provider family (Stripe, PayPal) can be organized in its own subdirectory, mirroring NestJS conventions.
- Dependency injection: The concrete product classes are created by the factory, not by the IoC container. The container manages the factories themselves.
When to Use
- Your system must work with multiple families of related products (e.g., payment processors and refund handlers from the same provider).
- You need to ensure that products from different families are not accidentally mixed.
- You want to add new product families without modifying existing client code.
- You want to encapsulate platform or vendor differences behind a common interface.
When NOT to Use
- When there is only one family of products and no realistic chance of adding more.
- When the products in a family are unrelated and do not need to be kept consistent. Separate Factory Methods are simpler.
- When the number of product types in a family changes frequently, forcing changes in every concrete factory.
Related Patterns
Factory Method
Define an interface for creating an object, but let subclasses decide which class to instantiate.
Singleton
Ensure a class has only one instance and provide a global point of access to it.
Builder
Separate the construction of a complex object from its representation so that the same construction process can create different representations.