Design Patterns & Practices  

Abstract Factory Pattern in .NET (Step-by-Step Guide with Real-World Example)

Introduction

Abstract Factory Pattern is a creational design pattern that provides an interface for creating multiple or dependent objects without specifying their concrete classes.

It basically has:

  • An abstract factory interface that defines a set of methods for creating related objects.

  • Multiple concrete factory classes that implement this interface to create specific variants of those related objects.

  • A set of abstract interfaces or type definitions that define the contracts for the objects being created.

  • Multiple concrete implementations of those abstract types, organized into groups.

The Abstract Factory Pattern allows you to create related objects in a consistent way without tightly coupling code to specific concrete classes.

Prerequisites

  • Visual Studio 2022 installed

  • Basics of .NET

Source Code

The source code used in the example is publicly available on GitHub: Link

Why Do We Need Abstract Factory If We Already Have Factory Method?

The Factory Method Pattern defines a method for creating a single object, but it lets subclasses decide which concrete class to instantiate. Whereas, Abstract Factory provides an interface to create multiple related objects.

Step-by-Step Implementation with Real-World Example

Let’s say you’re building an e-commerce platform. You support Credit Card and PayPal.

Wait, it’s not that simple.

Each payment provider requires:

  • Payment Processor

  • Refund Processor

  • Transaction Logger

And here comes the important part:

If you choose Stripe, all related components must be Stripe-specific.

If you choose PayPal, everything must belong to PayPal.

If you write code in this way:

new StripePaymentProcessor()
new PayPalRefundProcessor()

Congratulations! You have mixed everything and will create code maintenance chaos as the system grows.

Can We Solve This Using Factory Method Pattern?

Factory Method works great if you create only one object.

public interface IPaymentProcessor
{
    void Process(decimal amount);
}

And you create the object like this:

public class PaymentFactory
{
    public static IPaymentProcessor Create(string provider)
    {
        return provider switch
        {
            "Stripe" => new StripePaymentProcessor(),
            "PayPal" => new PayPalPaymentProcessor(),
            _ => throw new ArgumentException()
        };
    }
}

This is nice, but you also need:

  • IRefundProcessor

  • ITransactionLogger

Now your factory becomes:

  • CreatePaymentProcessor()

  • CreateRefundProcessor()

  • CreateLogger()

And every method needs the same switch logic.

Implementing with Abstract Factory Pattern

Define the Abstract Contracts

public interface IPaymentProcessor
{
    void Process(decimal amount);
}

public interface IRefundProcessor
{
    void Refund(decimal amount);
}

public interface ITransactionLogger
{
    void Log(string message);
}

Stripe Implementation

public class StripePaymentProcessor : IPaymentProcessor
{
    public void Process(decimal amount)
    {
        Console.WriteLine($"[Stripe] Processing payment of {amount:C}");
    }
}

public class StripeRefundProcessor : IRefundProcessor
{
    public void Refund(decimal amount)
    {
        Console.WriteLine($"[Stripe] Refunding {amount:C}");
    }
}

public class StripeTransactionLogger : ITransactionLogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[Stripe Log] {message}");
    }
}

PayPal Implementation

public class PayPalPaymentProcessor : IPaymentProcessor
{
    public void Process(decimal amount)
    {
        Console.WriteLine($"[PayPal] Processing payment of {amount:C}");
    }
}

public class PayPalRefundProcessor : IRefundProcessor
{
    public void Refund(decimal amount)
    {
        Console.WriteLine($"[PayPal] Refunding {amount:C}");
    }
}

public class PayPalTransactionLogger : ITransactionLogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[PayPal Log] {message}");
    }
}

Create the Abstract Factory

public interface IPaymentGatewayFactory
{
    IPaymentProcessor CreatePaymentProcessor();
    IRefundProcessor CreateRefundProcessor();
    ITransactionLogger CreateTransactionLogger();
}

Stripe Factory Implementation

public class StripeFactory : IPaymentGatewayFactory
{
    public IPaymentProcessor CreatePaymentProcessor()
        => new StripePaymentProcessor();

    public IRefundProcessor CreateRefundProcessor()
        => new StripeRefundProcessor();

    public ITransactionLogger CreateTransactionLogger()
        => new StripeTransactionLogger();
}

PayPal Factory Implementation

public class PayPalFactory : IPaymentGatewayFactory
{
    public IPaymentProcessor CreatePaymentProcessor()
        => new PayPalPaymentProcessor();

    public IRefundProcessor CreateRefundProcessor()
        => new PayPalRefundProcessor();

    public ITransactionLogger CreateTransactionLogger()
        => new PayPalTransactionLogger();
}

Implementing PaymentService

public class PaymentService
{
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly IRefundProcessor _refundProcessor;
    private readonly ITransactionLogger _logger;

    public PaymentService(IPaymentGatewayFactory factory)
    {
        _paymentProcessor = factory.CreatePaymentProcessor();
        _refundProcessor = factory.CreateRefundProcessor();
        _logger = factory.CreateTransactionLogger();
    }

    public void MakePayment(decimal amount)
    {
        _logger.Log("Starting payment...");
        _paymentProcessor.Process(amount);
        _logger.Log("Payment completed.");
    }

    public void RefundPayment(decimal amount)
    {
        _logger.Log("Starting refund...");
        _refundProcessor.Refund(amount);
        _logger.Log("Refund completed.");
    }
}

Using the Factory

From the entire family of objects, we can choose a specific ecosystem, such as PayPal in this example.

IPaymentGatewayFactory factory = new PayPalFactory();
var paymentService = new PaymentService(factory);

paymentService.MakePayment(100);
paymentService.RefundPayment(50);

Here we choose to use the PayPal factory. All the processes like Payment, Refund, and Logging will be handled via PayPal. This can be observed in the program output.

Conclusion

When a system starts supporting multiple families of related objects like payment providers, database engines, UI themes, or cloud providers, complexity grows quickly. Conditionals and switch statements begin to appear everywhere. Business logic becomes noisy.

Abstract Factory solves one very specific problem: How do I switch entire ecosystems of related objects without touching core logic?

In our payment example, the moment we wrote:

IPaymentGatewayFactory factory = new PayPalFactory();

The complete logic switches to the PayPal processor.

Summary

The Abstract Factory Pattern is used when a system needs to create families of related objects without mixing implementations. Instead of using multiple switch statements, we delegate object creation to a factory that guarantees consistency within an ecosystem (such as Stripe or PayPal). This keeps business logic clean, scalable, and maintainable while allowing entire systems to switch behavior simply by changing the factory implementation.