Imagine an e-commerce system that handles different payment methods: credit cards, PayPal, bank transfers. Each one has its own logic, validations and external providers. If you create them with new wherever you need them, you end up with this:
// Naive approach - what NOT to do public void ProcessPayment(string paymentType, decimal amount) { if (paymentType == "CreditCard") { var processor = new CreditCardProcessor(); processor.Process(amount); } else if (paymentType == "PayPal") { var processor = new PayPalProcessor(); processor.Process(amount); } // ... 20 more payment methods } Rigid, hard to maintain, impossible to extend without touching it. The Factory Method exists to separate creating an object from using it.
The classic structure
The Gang of Four defined it like this:
Define an interface for creating an object, but let subclasses decide which class to instantiate.
Four roles: the Product (an interface), the ConcreteProducts, the Creator that declares the factory method, and the ConcreteCreators that implement it.
public interface IPaymentProcessor { void Process(decimal amount); } public class CreditCardProcessor : IPaymentProcessor { public void Process(decimal amount) => Console.WriteLine($"Credit card payment: {amount}"); } public abstract class PaymentProcessorCreator { // The factory method protected abstract IPaymentProcessor CreateProcessor(); // The operation that uses the product public void ProcessPayment(decimal amount) => CreateProcessor().Process(amount); } public class CreditCardProcessorCreator : PaymentProcessorCreator { protected override IPaymentProcessor CreateProcessor() => new CreditCardProcessor(); } It's robust, but it costs one creator class per product, and the hierarchy grows fast.
The pragmatic variant: a simple factory
In many applications the full structure is too much, so the decision moves into one method. Modern C# pattern matching makes it short and explicit:
public enum PaymentType { CreditCard, PayPal, BankTransfer } public record PaymentRequest(PaymentType Type, decimal Amount); public static class PaymentProcessorFactory { public static IPaymentProcessor Create(PaymentRequest request) => request switch { { Type: PaymentType.CreditCard, Amount: > 10_000m } => new HighValueCreditCardProcessor(), { Type: PaymentType.CreditCard } => new CreditCardProcessor(), { Type: PaymentType.PayPal } => new PayPalProcessor(), _ => throw new NotSupportedException($"No processor for {request.Type}") }; } The trade-off: adding a payment type means changing the factory.
In modern .NET: keyed services
Since .NET 8, the DI container can register several implementations of the same interface under different keys. The factory becomes a thin, typed wrapper:
builder.Services.AddKeyedTransient<IPaymentProcessor, CreditCardProcessor>(PaymentType.CreditCard); builder.Services.AddKeyedTransient<IPaymentProcessor, PayPalProcessor>(PaymentType.PayPal); builder.Services.AddScoped<IPaymentProcessorFactory, KeyedPaymentProcessorFactory>(); public interface IPaymentProcessorFactory { IPaymentProcessor CreateProcessor(PaymentType type); } // The provider stays inside the factory: callers see only a typed method public sealed class KeyedPaymentProcessorFactory(IServiceProvider provider) : IPaymentProcessorFactory { public IPaymentProcessor CreateProcessor(PaymentType type) => provider.GetRequiredKeyedService<IPaymentProcessor>(type); } public class CheckoutService(IPaymentProcessorFactory factory) { public void Pay(PaymentType type, decimal amount) => factory.CreateProcessor(type).Process(amount); } A new payment method is a new class plus one registration. The processors get their own dependencies from the container, and CheckoutService never sees IServiceProvider.
When to use it, and when not
Use it when:
- creation depends on conditions known only at runtime (type, amount, country, risk);
- creating the object involves more than
new: configuration, credentials, remote resources (this is where asynchronous factories come in); - you want one place to extend when a new variant arrives.
Skip it when:
- creation is straightforward: the DI container is enough;
- the objects are simple, stateless and without dependencies;
- they're value objects or DTOs: a factory only adds ceremony.
Guiding principles
- Prefer Dependency Injection over hand-written factories when it's enough.
- Keep
IServiceProviderinside the factory, and expose only typed creation methods. - Use pattern matching for complex conditional logic.
- Keep it simple: don't over-engineer simple scenarios.
Want the full story?
This is the short version. The complete article on Level Up Coding follows the Factory Method through three decades: the GoF structure and its UML, the simple factory, reflection and configuration-driven factories, generics, Dependency Injection, asynchronous factories for the cloud, distributed factories with service discovery and fallbacks, factories generated at compile time with Source Generators, and the recommended modern implementations.
Read it here: Factory Method Pattern in .NET C#: Evolution and complete guide 2026.
Thanks for reading.

