BrandGhost
Factory Method Pattern Real-World Example in C#: Complete Implementation

Factory Method Pattern Real-World Example in C#: Complete Implementation

Factory Method Pattern Real-World Example in C#: Complete Implementation

Design pattern tutorials often use contrived examples like shapes and animals, which makes it hard to see how a pattern actually fits into production code. This article takes a different approach. We'll build a complete Factory Method pattern real-world example in C# using a payment processing system — the kind of system you'd find in an actual e-commerce or SaaS application. You'll see how the Factory Method pattern solves real extensibility challenges and why it's the right tool for this particular job.

By working through this implementation step by step, you'll understand not just the mechanics of the pattern but the reasoning behind each design decision. We'll start with the problem statement, build the solution incrementally, and then extend it to demonstrate the pattern's flexibility.

The Problem: A Payment Processing System

Imagine you're building a payment processing module for an e-commerce platform. The platform needs to support multiple payment providers: Stripe for credit card processing, PayPal for digital wallet payments, and bank transfers for enterprise customers. Each provider has different APIs, authentication mechanisms, and response formats.

The naive approach is to use conditional logic throughout your codebase:

// The problem: scattered conditional creation
public class OrderService
{
    public PaymentResult ProcessPayment(
        Order order, string provider)
    {
        if (provider == "stripe")
        {
            var stripe = new StripePaymentHandler();
            stripe.SetApiKey(config["StripeKey"]);
            return stripe.Charge(order.Total);
        }
        else if (provider == "paypal")
        {
            var paypal = new PayPalPaymentHandler();
            paypal.SetClientId(config["PayPalClientId"]);
            paypal.SetSecret(config["PayPalSecret"]);
            return paypal.ExecutePayment(order.Total);
        }
        else if (provider == "bank")
        {
            var bank = new BankTransferHandler();
            bank.SetRoutingInfo(config["BankRouting"]);
            return bank.InitiateTransfer(order.Total);
        }

        throw new ArgumentException(
            $"Unknown provider: {provider}");
    }
}

This code has several problems. Every time you add a new payment provider, you must modify the OrderService class. The creation logic, configuration, and usage are all tangled together. Testing requires dealing with all providers even when you only want to test one. This is exactly the kind of problem the Factory Method pattern solves.

Step 1: Define the Product Interface

The first step in our Factory Method pattern real-world example in C# is defining the abstraction that all payment handlers will implement. This interface captures the essential behaviors every payment provider must support:

// The Product interface defines what every
// payment handler must be able to do
public interface IPaymentHandler
{
    string ProviderName { get; }

    Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request);

    Task<RefundResult> ProcessRefundAsync(
        string transactionId,
        decimal amount);

    Task<PaymentStatus> CheckStatusAsync(
        string transactionId);
}

// Supporting types for the payment system
public record PaymentRequest(
    decimal Amount,
    string Currency,
    string CustomerEmail,
    Dictionary<string, string> Metadata);

public record PaymentResult(
    bool Success,
    string TransactionId,
    string Message);

public record RefundResult(
    bool Success,
    string RefundId,
    string Message);

public enum PaymentStatus
{
    Pending,
    Completed,
    Failed,
    Refunded
}

Notice that the interface uses async methods, which is realistic for payment processing where network calls are involved. The supporting record types keep the data contracts clean and immutable.

Step 2: Build the Concrete Products

Each payment provider implements the IPaymentHandler interface with its own specific logic. In this real-world example, we'll implement three providers that represent the most common payment methods in e-commerce systems. Each handler encapsulates the behavior specific to its provider, including how it communicates with external APIs, handles errors, and reports status.

Stripe Payment Handler

public class StripePaymentHandler : IPaymentHandler
{
    public string ProviderName => "Stripe";

    public async Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request)
    {
        // In production, this would call the Stripe API
        Console.WriteLine(
            $"[Stripe] Processing {request.Currency} " +
            $"{request.Amount} for {request.CustomerEmail}");

        await Task.Delay(100); // Simulate API call

        var transactionId = $"stripe_{Guid.NewGuid():N}";
        return new PaymentResult(
            true, transactionId,
            "Payment processed via Stripe");
    }

    public async Task<RefundResult> ProcessRefundAsync(
        string transactionId, decimal amount)
    {
        Console.WriteLine(
            $"[Stripe] Refunding {amount} for " +
            $"transaction {transactionId}");

        await Task.Delay(50);

        return new RefundResult(
            true,
            $"refund_{Guid.NewGuid():N}",
            "Refund processed via Stripe");
    }

    public async Task<PaymentStatus> CheckStatusAsync(
        string transactionId)
    {
        await Task.Delay(30);
        return PaymentStatus.Completed;
    }
}

PayPal Payment Handler

public class PayPalPaymentHandler : IPaymentHandler
{
    public string ProviderName => "PayPal";

    public async Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request)
    {
        Console.WriteLine(
            $"[PayPal] Creating payment of " +
            $"{request.Currency} {request.Amount}");

        await Task.Delay(150); // PayPal API tends to
                               // be slightly slower

        var transactionId = $"paypal_{Guid.NewGuid():N}";
        return new PaymentResult(
            true, transactionId,
            "Payment processed via PayPal");
    }

    public async Task<RefundResult> ProcessRefundAsync(
        string transactionId, decimal amount)
    {
        Console.WriteLine(
            $"[PayPal] Issuing refund of {amount}");

        await Task.Delay(100);

        return new RefundResult(
            true,
            $"pp_refund_{Guid.NewGuid():N}",
            "Refund processed via PayPal");
    }

    public async Task<PaymentStatus> CheckStatusAsync(
        string transactionId)
    {
        await Task.Delay(50);
        return PaymentStatus.Completed;
    }
}

Bank Transfer Handler

public class BankTransferHandler : IPaymentHandler
{
    public string ProviderName => "BankTransfer";

    public async Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request)
    {
        Console.WriteLine(
            $"[Bank] Initiating transfer of " +
            $"{request.Currency} {request.Amount}");

        await Task.Delay(200); // Bank transfers take
                               // longer to initiate

        var transactionId = $"bank_{Guid.NewGuid():N}";
        // Bank transfers are pending until confirmed
        return new PaymentResult(
            true, transactionId,
            "Transfer initiated - pending confirmation");
    }

    public async Task<RefundResult> ProcessRefundAsync(
        string transactionId, decimal amount)
    {
        Console.WriteLine(
            $"[Bank] Processing refund of {amount}");

        await Task.Delay(150);

        return new RefundResult(
            true,
            $"bank_ref_{Guid.NewGuid():N}",
            "Bank refund initiated");
    }

    public async Task<PaymentStatus> CheckStatusAsync(
        string transactionId)
    {
        await Task.Delay(80);
        // Bank transfers stay pending longer
        return PaymentStatus.Pending;
    }
}

Each handler encapsulates the specific behavior of its payment provider. The Stripe handler processes immediately, PayPal has slightly different timing, and bank transfers return a pending status. These are realistic differences you'd encounter when integrating with actual payment APIs.

Step 3: Create the Abstract Creator

The abstract creator is the cornerstone of the Factory Method pattern. It declares the factory method and provides business logic that uses the created product without knowing its concrete type. This is where the pattern's power becomes evident — the abstract creator defines the workflow, while subclasses control which specific handler participates in that workflow:

// The Creator with the factory method and
// business logic that uses the product
public abstract class PaymentProcessorCreator
{
    // The Factory Method - subclasses decide
    // which handler to instantiate
    public abstract IPaymentHandler CreateHandler();

    // Template method using the factory method
    public async Task<PaymentResult> ProcessOrderPaymentAsync(
        Order order)
    {
        var handler = CreateHandler();

        Console.WriteLine(
            $"Processing order {order.OrderId} via " +
            $"{handler.ProviderName}");

        var request = new PaymentRequest(
            order.Total,
            order.Currency,
            order.CustomerEmail,
            new Dictionary<string, string>
            {
                ["orderId"] = order.OrderId,
                ["source"] = "web"
            });

        var result = await handler.ProcessPaymentAsync(
            request);

        if (result.Success)
        {
            Console.WriteLine(
                $"Payment successful: {result.TransactionId}");
        }
        else
        {
            Console.WriteLine(
                $"Payment failed: {result.Message}");
        }

        return result;
    }

    // Another method demonstrating the pattern
    public async Task<RefundResult> ProcessRefundAsync(
        string transactionId, decimal amount)
    {
        var handler = CreateHandler();

        Console.WriteLine(
            $"Processing refund via {handler.ProviderName}");

        return await handler.ProcessRefundAsync(
            transactionId, amount);
    }
}

// Supporting order type
public record Order(
    string OrderId,
    decimal Total,
    string Currency,
    string CustomerEmail);

The ProcessOrderPaymentAsync method demonstrates a template method that uses the factory method. The business logic for processing an order payment is defined in the abstract creator, while the specific payment handler is provided by subclasses. This separation keeps the business workflow consistent across all payment providers.

Step 4: Implement the Concrete Creators

Each concrete creator overrides the factory method to return the appropriate payment handler. These classes are intentionally simple because their only responsibility is deciding which product to create:

public class StripePaymentCreator
    : PaymentProcessorCreator
{
    public override IPaymentHandler CreateHandler()
    {
        return new StripePaymentHandler();
    }
}

public class PayPalPaymentCreator
    : PaymentProcessorCreator
{
    public override IPaymentHandler CreateHandler()
    {
        return new PayPalPaymentHandler();
    }
}

public class BankTransferCreator
    : PaymentProcessorCreator
{
    public override IPaymentHandler CreateHandler()
    {
        return new BankTransferHandler();
    }
}

Each creator is minimal and focused. There's no conditional logic, no configuration tangling, and no reason to modify these classes unless the way their specific handler is constructed changes.

Step 5: Wire It Together

Now let's see the complete system in action. The client code works with the abstract creator and can switch between payment providers without any code changes:

public class CheckoutService
{
    private readonly PaymentProcessorCreator _paymentCreator;

    // The creator is injected — client never
    // references concrete handlers directly
    public CheckoutService(
        PaymentProcessorCreator paymentCreator)
    {
        _paymentCreator = paymentCreator;
    }

    public async Task<PaymentResult> CheckoutAsync(
        Order order)
    {
        Console.WriteLine(
            $"Starting checkout for order {order.OrderId}");

        var result = await _paymentCreator
            .ProcessOrderPaymentAsync(order);

        if (result.Success)
        {
            Console.WriteLine("Checkout complete!");
        }

        return result;
    }
}

// Usage example
var order = new Order(
    "ORD-12345", 99.99m, "USD", "[email protected]");

// Process with Stripe
var stripeCheckout = new CheckoutService(
    new StripePaymentCreator());
await stripeCheckout.CheckoutAsync(order);

// Process with PayPal — same interface, different provider
var paypalCheckout = new CheckoutService(
    new PayPalPaymentCreator());
await paypalCheckout.CheckoutAsync(order);

The CheckoutService has no knowledge of Stripe, PayPal, or bank transfers. It works entirely through the abstract PaymentProcessorCreator interface. This is the practical power of the Factory Method pattern — the checkout logic is completely decoupled from the specific payment providers.

Extending the System

One of the greatest benefits of the Factory Method pattern in this real-world scenario is how easy it is to extend. Let's say the business wants to add cryptocurrency payments. With the Factory Method pattern in place, you only need to create two new classes without touching any existing code. No changes to the checkout service, no changes to the abstract creator, and no changes to any existing payment handlers:

// New product — no existing code changes needed
public class CryptoPaymentHandler : IPaymentHandler
{
    public string ProviderName => "Crypto";

    public async Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request)
    {
        Console.WriteLine(
            $"[Crypto] Processing {request.Amount} " +
            $"crypto payment");

        await Task.Delay(300); // Blockchain confirmation

        return new PaymentResult(
            true,
            $"crypto_{Guid.NewGuid():N}",
            "Crypto payment submitted to blockchain");
    }

    public Task<RefundResult> ProcessRefundAsync(
        string transactionId, decimal amount)
    {
        return Task.FromResult(new RefundResult(
            false, "",
            "Crypto refunds are not supported"));
    }

    public async Task<PaymentStatus> CheckStatusAsync(
        string transactionId)
    {
        await Task.Delay(100);
        return PaymentStatus.Pending;
    }
}

// New creator — no existing code changes needed
public class CryptoPaymentCreator
    : PaymentProcessorCreator
{
    public override IPaymentHandler CreateHandler()
    {
        return new CryptoPaymentHandler();
    }
}

That's it. No modifications to CheckoutService, no changes to the abstract creator, no touching the existing payment handlers. The Open/Closed Principle is fully satisfied. The system is open for extension (new payment providers) and closed for modification (existing code remains untouched).

Integrating with Dependency Injection

In a production ASP.NET Core application, you'd register the creators with the DI container and resolve them based on configuration or runtime context:

// Register creators in Program.cs
builder.Services.AddKeyedSingleton<PaymentProcessorCreator>(
    "stripe", (_, _) => new StripePaymentCreator());
builder.Services.AddKeyedSingleton<PaymentProcessorCreator>(
    "paypal", (_, _) => new PayPalPaymentCreator());
builder.Services.AddKeyedSingleton<PaymentProcessorCreator>(
    "bank", (_, _) => new BankTransferCreator());

// Resolve based on customer preference
app.MapPost("/checkout", async (
    [FromBody] CheckoutRequest request,
    [FromKeyedServices("stripe")]
    PaymentProcessorCreator defaultCreator,
    IServiceProvider sp) =>
{
    // Choose creator based on payment method
    var creator = sp.GetKeyedService<PaymentProcessorCreator>(
        request.PaymentMethod) ?? defaultCreator;

    var checkout = new CheckoutService(creator);
    var order = new Order(
        request.OrderId,
        request.Amount,
        "USD",
        request.Email);

    return await checkout.CheckoutAsync(order);
});

This integration leverages inversion of control to manage factory lifecycle while preserving the polymorphic creation capabilities of the Factory Method pattern.

Testing the Factory Method Implementation

The pattern makes testing straightforward because you can create test-specific factories that return controlled implementations. This testability advantage is one of the strongest arguments for using the Factory Method pattern in production systems. Without the pattern, testing payment logic would require mocking actual payment provider APIs, dealing with network calls, and managing API keys in test environments.

With the Factory Method pattern, you simply create a test handler that behaves predictably:

// Test-specific handler for predictable results
public class TestPaymentHandler : IPaymentHandler
{
    public string ProviderName => "Test";
    public bool ShouldSucceed { get; set; } = true;

    public Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request)
    {
        return Task.FromResult(new PaymentResult(
            ShouldSucceed,
            "test_transaction_123",
            ShouldSucceed ? "Success" : "Failed"));
    }

    public Task<RefundResult> ProcessRefundAsync(
        string transactionId, decimal amount)
    {
        return Task.FromResult(new RefundResult(
            true, "test_refund_123", "Refunded"));
    }

    public Task<PaymentStatus> CheckStatusAsync(
        string transactionId)
    {
        return Task.FromResult(PaymentStatus.Completed);
    }
}

// Test creator
public class TestPaymentCreator : PaymentProcessorCreator
{
    private readonly TestPaymentHandler _handler = new();

    public TestPaymentHandler Handler => _handler;

    public override IPaymentHandler CreateHandler()
        => _handler;
}

// Unit test example
[Fact]
public async Task Checkout_WithSuccessfulPayment_Completes()
{
    // Arrange
    var creator = new TestPaymentCreator();
    var service = new CheckoutService(creator);
    var order = new Order(
        "TEST-001", 50.00m, "USD", "[email protected]");

    // Act
    var result = await service.CheckoutAsync(order);

    // Assert
    Assert.True(result.Success);
    Assert.Equal("test_transaction_123",
        result.TransactionId);
}

[Fact]
public async Task Checkout_WithFailedPayment_ReturnsFailure()
{
    // Arrange
    var creator = new TestPaymentCreator();
    creator.Handler.ShouldSucceed = false;
    var service = new CheckoutService(creator);
    var order = new Order(
        "TEST-002", 75.00m, "USD", "[email protected]");

    // Act
    var result = await service.CheckoutAsync(order);

    // Assert
    Assert.False(result.Success);
}

The test creator provides complete control over the payment handler's behavior, allowing you to test the checkout workflow in isolation. This is a major advantage over the original approach where testing required mocking or stubbing actual payment provider APIs. You can simulate success, failure, partial refunds, and any other scenario your business logic needs to handle, all without any network calls or external dependencies.

Applying This Pattern to Other Domains

While this example focuses on payment processing, the same approach works in many other real-world scenarios. Document generation systems can use factory methods to create different exporters (PDF, CSV, Excel) with the same overall workflow. Notification systems can use this pattern to create email, SMS, or push notification handlers. Data import pipelines can use factories to create parsers for different file formats.

The key question to ask yourself is: "Do I have multiple implementations of the same behavior that need to be selected at runtime?" If the answer is yes, and those implementations are complex enough to warrant their own classes, the Factory Method pattern is likely a good fit. The design patterns catalog is full of creational patterns, but the Factory Method remains one of the most practical and frequently applicable.

Key Takeaways from This Implementation

This real-world implementation demonstrates several important principles that apply to any domain, not just payment processing. Understanding these takeaways will help you apply the Factory Method pattern effectively in your own projects.

The factory software pattern shines when you have multiple implementations of the same interface that need to be selected at runtime. Payment providers, notification channels, export formats, and other design patterns like the Strategy pattern all share this characteristic. When you recognize this pattern in your domain, you know the Factory Method is a strong candidate.

The template method in the abstract creator (ProcessOrderPaymentAsync) shows how to combine the Factory Method with other patterns for even more powerful designs. The workflow stays consistent while the specific handler varies by subclass. This combination is extremely common in production systems where the overall process is standardized but individual steps need to be customizable.

Extending the system requires creating new classes, not modifying existing ones. This is the Open/Closed Principle in practice, and it's the most compelling reason to use the Factory Method pattern in production systems. In a team environment, this means multiple developers can add new providers simultaneously without creating merge conflicts or risking regressions in existing provider implementations.

Frequently Asked Questions

Can this pattern handle payment providers with different configuration needs?

Yes. Each concrete creator can accept its own configuration through its constructor. For example, the StripePaymentCreator could accept a Stripe API key, while PayPalPaymentCreator accepts a client ID and secret. The abstract creator doesn't need to know about these configuration differences.

How does this compare to using a switch statement to select payment providers?

A switch statement centralizes the selection logic but violates the Open/Closed Principle. Every new provider requires modifying the switch statement. The Factory Method pattern moves each provider's creation logic into its own class, so adding a new provider never touches existing code.

Should I use this pattern if I only have one payment provider?

Probably not. If you're certain you'll only ever have one provider, the Factory Method pattern adds unnecessary abstraction. However, if there's a reasonable chance you'll add providers in the future, the pattern is worth the small upfront investment because retrofitting it later is more disruptive.

How do I handle provider-specific features that aren't in the common interface?

Use interface composition. Create additional interfaces for specific capabilities (like ICryptoPaymentHandler with a GetBlockchainAddress method) and use pattern matching to check if the handler supports those features. This keeps the common interface clean while allowing access to provider-specific functionality.

What if the factory method needs to create handlers with different constructor parameters?

Inject the dependencies into the concrete creator class. The creator then passes the appropriate dependencies to the handler during construction. This keeps the factory method signature clean while supporting complex handler construction.

Can I combine this with the Abstract Factory pattern?

Absolutely. If your payment system needs families of related objects (a processor, a validator, and a receipt generator for each provider), the Abstract Factory pattern would be more appropriate. The Factory Method works well here because we're creating a single product type per factory.

How does error handling work across different payment providers?

Each concrete handler manages its own error handling internally. The abstract creator can wrap the factory method call in try/catch to handle common error scenarios. Provider-specific errors should be translated into your application's error model within each handler, keeping the error handling consistent at the creator level.

Wrapping Up This Factory Method Real-World Example

This payment processing implementation shows how the Factory Method pattern transforms a real-world problem from rigid, hard-to-maintain code into a flexible, extensible architecture. The pattern isn't about adding complexity — it's about putting complexity in the right places so that future changes are easy and safe.

We started with a problematic OrderService that used scattered conditional logic and ended with a clean architecture where each payment provider is isolated in its own handler and factory. The checkout service works entirely through abstractions, making it testable, extensible, and easy to understand.

The key insight from this example is that the Factory Method pattern creates a clear boundary between what your system does (process payments) and how it does it (through specific providers). That boundary is what makes the system maintainable as it grows and evolves. Apply this same thinking to your own domain, and you'll find many opportunities where the Factory Method pattern can simplify your architecture while making your codebase more resilient to change.

Builder Pattern Real-World Example in C#: Complete Implementation

See Builder pattern in action with a complete real-world C# example. Step-by-step implementation of a configuration system demonstrating step-by-step object construction.

Factory Method Design Pattern in C#: Complete Guide

Master the Factory Method design pattern in C# with code examples, real-world scenarios, and practical guidance for flexible object creation.

Abstract Factory Pattern Real-World Example in C#: Complete Implementation

See Abstract Factory pattern in action with a complete real-world C# example. Step-by-step implementation of a furniture shop system demonstrating families of related objects.

An error has occurred. This application may no longer respond until reloaded. Reload