Creational patterns answer one question: who decides which concrete class gets built? Factory says: not the caller.
Imagine a checkout flow that needs to charge a customer. Somewhere, code has to write new StripeGateway() or new PayPalGateway() or new WireTransferGateway(). The question is: where? If the answer is "scattered across every controller, every service, every place a payment happens to occur," you have a problem — the moment you add a fourth payment provider, or the business rule for choosing one changes, you're hunting through the entire codebase for every new SomeGateway() call.
The Factory Pattern family exists to answer a deceptively simple question: who decides which concrete class gets constructed, and where does that decision live? Instead of every caller making that decision independently, one place makes it — a factory — and everyone else just asks for "the right thing" without knowing or caring how it's built.
In this lesson, you'll learn the three recognized flavors of the Factory pattern — Simple Factory, Factory Method, and Abstract Factory — how they differ, when each one earns its complexity, and how they relate to something you already use every day: the DI container.
A factory is code whose entire job is deciding which concrete class to instantiate and handing back the result — usually through a common interface or base type — so that the calling code never has to write new ConcreteClassName() itself, or even know that class name exists.
This is a Creational pattern — one of the three GoF categories covered in the design patterns lesson earlier in this Part. Creational patterns are all about controlling and centralizing how objects get created, as opposed to Structural patterns (how objects are composed) or Behavioral patterns (how objects communicate).
"Factory" isn't one single pattern — it's a family of three, each solving a slightly different shaped problem. Getting them straight matters, because mixing up their names is one of the most common mistakes developers make when discussing design patterns:
virtual/abstract methodA factory that creates a whole family of related objects together, guaranteeing they're compatible with each other. Where Simple Factory and Factory Method each produce one kind of thing, Abstract Factory produces a matched set — e.g. a UI theme factory that hands back a Button, a Checkbox, and a ScrollBar that are all guaranteed to be the "Dark theme" versions, never an accidental mix of Dark and Light widgets.
Without a factory, object creation logic — often full of conditionals — leaks into every place that needs an object:
// WITHOUT a factory — this conditional is copy-pasted everywhere a gateway is needed
public async Task<PaymentResult> CheckoutAsync(Order order, string paymentMethod)
{
IPaymentGateway gateway = paymentMethod switch
{
"stripe" => new StripeGateway(_stripeApiKey),
"paypal" => new PayPalGateway(_payPalClientId, _payPalSecret),
"wire" => new WireTransferGateway(_bankRoutingInfo),
_ => throw new NotSupportedException($"Unknown payment method: {paymentMethod}")
};
return await gateway.ChargeAsync(order.Total);
}
// Every controller, every background job, every place a payment happens
// now needs to know EVERY concrete gateway class, its constructor arguments,
// and the full up-to-date list of valid paymentMethod strings.
This has three concrete costs: the construction logic is duplicated everywhere a gateway is needed; every caller is coupled to every concrete gateway class and its constructor signature, even callers that only ever use one of them; and adding a new payment method means hunting down and editing every duplicate of that switch statement.
All three flavors share the same shape: the caller depends on an abstraction and asks a factory for an instance of it — the caller never writes the concrete class name.
new StripeGateway() / new PayPalGateway() / new WireTransferGateway() — decision + construction logic baked directly into the callerPaymentGatewayFactory.Create(paymentMethod) → returns IPaymentGateway — the factory alone knows about Stripe, PayPal, and WireTransfer classes| Pattern | Produces | Mechanism |
|---|---|---|
| Simple Factory | One object | A method with a conditional |
| Factory Method | One object | A virtual method, overridden by subclasses |
| Abstract Factory | A family of related objects | An interface with multiple "create" methods, multiple implementations |
IPaymentGateway — the only type the rest of the application needs to know aboutStripeGateway, PayPalGateway, WireTransferGateway — each implements IPaymentGatewayPaymentGatewayFactory.Create(paymentMethod) — the only place in the codebase that mentions all three concrete class namesIPaymentGateway gateway = _factory.Create(order.PaymentMethod);public interface IPaymentGateway
{
Task<PaymentResult> ChargeAsync(decimal amount);
}
public sealed class StripeGateway(string apiKey) : IPaymentGateway
{
public Task<PaymentResult> ChargeAsync(decimal amount) =>
Task.FromResult(PaymentResult.Success("stripe-txn-id"));
}
public sealed class PayPalGateway(string clientId, string secret) : IPaymentGateway
{
public Task<PaymentResult> ChargeAsync(decimal amount) =>
Task.FromResult(PaymentResult.Success("paypal-txn-id"));
}
// ── The Simple Factory: one method, one decision, centralized ──
public static class PaymentGatewayFactory
{
public static IPaymentGateway Create(string paymentMethod) => paymentMethod switch
{
"stripe" => new StripeGateway(Config.StripeApiKey),
"paypal" => new PayPalGateway(Config.PayPalClientId, Config.PayPalSecret),
_ => throw new NotSupportedException($"Unknown payment method: {paymentMethod}")
};
}
// ── Usage — the caller never writes a concrete class name ──
IPaymentGateway gateway = PaymentGatewayFactory.Create(order.PaymentMethod);
var result = await gateway.ChargeAsync(order.Total);
Code → Meaning → Result: PaymentGatewayFactory.Create is the single place that knows both concrete class names and their constructor requirements. The calling code above only ever mentions IPaymentGateway. Add a fourth payment method tomorrow, and exactly one file changes — the factory. Every caller keeps working, unmodified.
The same shape shows up constantly for notification systems, where the "right" sender depends on user preference or channel configuration read at runtime:
public interface INotificationSender
{
Task SendAsync(string recipient, string message);
}
public sealed class EmailSender : INotificationSender { /* SMTP details */ }
public sealed class SmsSender : INotificationSender { /* SMS gateway details */ }
public sealed class PushSender : INotificationSender { /* push provider details */ }
// A factory that's itself a small injectable class — not just static methods —
// so it can depend on configuration and be unit tested like anything else.
public sealed class NotificationSenderFactory(IOptions<NotificationSettings> settings)
{
public INotificationSender CreateFor(NotificationChannel channel) => channel switch
{
NotificationChannel.Email => new EmailSender(),
NotificationChannel.Sms => new SmsSender(),
NotificationChannel.Push => new PushSender(),
_ => throw new NotSupportedException($"Unsupported channel: {channel}")
};
}
// ── Usage in an OrderShippedHandler ──
public sealed class OrderShippedHandler(NotificationSenderFactory senderFactory)
{
public async Task HandleAsync(Order order, NotificationChannel preferredChannel)
{
var sender = senderFactory.CreateFor(preferredChannel);
await sender.SendAsync(order.CustomerContact, $"Order {order.Id} has shipped!");
}
}
Notice the factory here is itself a small, DI-friendly class rather than a bag of static methods — that's a deliberate, common variation: the factory can take dependencies of its own (like configuration), and the factory itself can be registered and injected, without changing the pattern's fundamental shape.
Factory Method solves a related but distinct problem: a base class defines an algorithm's overall shape, but wants subclasses to control which concrete type one step of that algorithm produces:
public abstract class ReportGenerator
{
// The algorithm — fixed, lives in the base class, never overridden
public string Generate(ReportData data)
{
var exporter = CreateExporter(); // ← the "factory method"
var body = exporter.Export(data);
return $"--- Report: {DateTime.UtcNow:d} ---\n{body}";
}
protected abstract IReportExporter CreateExporter(); // subclasses decide WHAT gets created
}
public sealed class PdfReportGenerator : ReportGenerator
{
protected override IReportExporter CreateExporter() => new PdfExporter();
}
public sealed class CsvReportGenerator : ReportGenerator
{
protected override IReportExporter CreateExporter() => new CsvExporter();
}
// ReportGenerator.Generate never changes; only WHICH exporter gets built varies by subclass.
ReportGenerator generator = new PdfReportGenerator();
string report = generator.Generate(data);
The difference from Simple Factory is mechanical but important: there's no switch statement anywhere — the "decision" is made by which subclass you instantiated, and enforced by the compiler through abstract/override. This is genuinely the Template Method pattern (a fixed algorithm skeleton with pluggable steps) applied specifically to object creation.
You order "the pasta" from the menu — you don't walk into the kitchen, gather the ingredients, and cook it yourself. The kitchen (the factory) decides exactly how to combine flour, eggs, and sauce into a finished dish, and hands you a plate (an object satisfying an interface: "edible food"). You never need to know the recipe.
Abstract Factory is like ordering a "themed meal" — a starter, main, and dessert that are all guaranteed to be from the same cuisine. You don't get an Italian starter with a Japanese dessert; the factory ensures the whole family is compatible.
An Abstract Factory looks like this — one interface exposing several "create" methods, with a concrete implementation per compatible family:
public interface IUiThemeFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
public sealed class DarkThemeFactory : IUiThemeFactory
{
public IButton CreateButton() => new DarkButton();
public ICheckbox CreateCheckbox() => new DarkCheckbox();
}
public sealed class LightThemeFactory : IUiThemeFactory
{
public IButton CreateButton() => new LightButton();
public ICheckbox CreateCheckbox() => new LightCheckbox();
}
// Whichever IUiThemeFactory you're handed, CreateButton() and CreateCheckbox()
// are GUARANTEED to return matching-theme widgets — no accidental Dark button
// next to a Light checkbox.
OrderService's constructor and hands it a concrete SqlInventoryRepository for its IInventoryRepository parameter (Intermediate 124, and Advanced 238's container-at-scale coverage), it is doing exactly what PaymentGatewayFactory.Create does — deciding which concrete type satisfies an abstraction and constructing it — just generalized across your entire object graph instead of one interface. Every services.AddScoped<IFoo, Foo>() registration is teaching the container's built-in factory logic one more "which concrete type" decision.
Close, but not quite — a DI container is closer to a generalized Simple Factory operating over your whole application, resolved by type rather than a hand-written switch. Factory Method specifically requires inheritance — a base class calling an abstract/virtual creation step that a subclass overrides. A DI container doesn't use inheritance to decide what to construct; it uses a registration lookup. Both are still "Factory family" ideas — object creation is centralized and abstracted away from the consumer — but they're mechanically different patterns.
Not for practical purposes. The original 1994 GoF catalog lists Factory Method and Abstract Factory, but not "Simple Factory" by that exact name — it's usually described as an idiom, or a simplified stepping stone toward Factory Method. In everyday conversation and in code reviews, "factory" almost always means this simple, centralized-conditional shape, and that's fine — just know that if someone asks you to name the 23 GoF patterns, "Simple Factory" isn't one of the official ones.
Writing GatewayFactory.Create(gatewayType) when there's really only ever going to be one IPaymentGateway implementation active per environment, chosen once at startup — that's not a runtime decision that needs a factory, it's just a registration: services.AddScoped<IPaymentGateway, StripeGateway>(). Reach for a hand-written factory specifically when the decision genuinely happens per-call, based on data only available at that moment (like order.PaymentMethod) — not when it's a fixed, environment-wide choice the container already handles for free.
Every new payment method means editing the switch inside PaymentGatewayFactory.Create directly — which is fine at 3 or 4 cases, but at 20 becomes a genuine maintenance hotspot everyone touches. For a large or frequently-growing family, consider registering gateways in the DI container keyed by a discriminator and resolving via IEnumerable<IPaymentGateway> plus a matching property on each implementation — trading a hand-written switch for container-driven lookup.
Building a full IUiThemeFactory-style abstraction with multiple Create...() methods when you only ever need to create one kind of object — that's Simple Factory or Factory Method wearing unnecessary Abstract Factory ceremony. Abstract Factory earns its complexity specifically when multiple related objects must be created together and must stay compatible with each other — if there's no compatibility constraint between the objects, you don't need the family concept at all.
And when it's overkill: if there's genuinely only one implementation of an interface that will ever exist in a given environment, a plain DI container registration already is your factory — writing a dedicated factory class on top adds a layer with nothing left to decide. Don't build a factory "just in case" a second implementation shows up someday; add it when the second implementation actually exists.
abstract step that subclasses fill in with a concrete type.You've seen all three flavors of Factory and how the DI container relates. Let's check you can tell them apart and know when each earns its keep.
1. A base class ReportGenerator defines a fixed Generate() algorithm, but calls an abstract method that subclasses override to determine which exporter type gets used. Which Factory variant is this?
Correct: B
Why B is correct: Factory Method is precisely a fixed algorithm in a base class that delegates one creation step to a virtual/abstract method overridden by subclasses — exactly the ReportGenerator/CreateExporter() shape shown in this lesson.
Why A is incorrect: Simple Factory uses a conditional in a standalone method, not inheritance and overriding.
Why C is incorrect: Abstract Factory creates a family of multiple related objects together — this example creates only one object (the exporter).
Why D is incorrect: It does use polymorphism as its mechanism, but the specific, named pattern for "fixed algorithm delegates a creation step to an overridden method" is Factory Method.
Reinforcement: Factory Method is identified by inheritance and an overridden creation step, not by a conditional.
2. Which scenario is the best fit for Abstract Factory specifically, rather than Simple Factory?
Correct: C
Why C is correct: Abstract Factory's defining trait is producing a family of related objects that must remain mutually compatible — exactly the UI theme scenario, where mixing a Dark button with a Light checkbox would be a bug.
Why A is incorrect: A single implementation chosen once at startup is simply a DI registration — no factory of any kind is needed here.
Why B is incorrect: This selects exactly one object per call, based on runtime data — a textbook Simple Factory case, not a family of compatible objects.
Why D is incorrect: This is also a single-object decision (which exception to construct), not a family requiring mutual compatibility.
Reinforcement: Reach for Abstract Factory only when multiple objects must be created together and stay compatible — not merely when multiple types exist.
3. Why is a DI container described in this lesson as "a generalized factory"?
Correct: B
Why B is correct: A DI container resolves an interface-typed constructor parameter to a registered concrete type and constructs it — the exact same "decide which concrete type, then construct it, return the abstraction" responsibility a hand-written factory has, just scaled to the whole application rather than one call site.
Why A is incorrect: The comparison is conceptual, about responsibility and behavior, not about naming conventions in the container's own source code.
Why C is incorrect: Performance isn't the basis for the comparison at all — this is about the shared responsibility of centralizing object-creation decisions.
Why D is incorrect: A DI container's resolution mechanism (registration lookup) doesn't require inheritance or a "family of related objects" — it's closer in shape to Simple Factory, generalized, not specifically Abstract Factory.
Reinforcement: Any mechanism that centralizes "which concrete type satisfies this abstraction" is philosophically part of the Factory family, even when it doesn't look like a hand-written factory class.
4. A team has exactly one IEmailSender implementation, chosen once via services.AddScoped<IEmailSender, SendGridSender>() at startup, with no plan to support a second provider. A developer proposes wrapping this in an EmailSenderFactory class "to follow the Factory pattern." Is this a good idea?
Correct: B
Why B is correct: This is precisely Mistake 1 from the lesson — when there's only one implementation, chosen once at startup, the DI container's registration already performs the entire job a factory would do. Wrapping it in a dedicated factory class adds a layer with no decision left to make.
Why A is incorrect: SOLID doesn't mandate a factory for every interface — this treats a design pattern as a checklist item rather than a tool applied where it solves a real problem.
Why C is incorrect: Whether the factory is static or instance-based has no bearing on whether a factory is needed in the first place.
Why D is incorrect: Nothing about SendGrid specifically is relevant; the issue is the absence of any runtime "which concrete type" decision to centralize.
Reinforcement: A factory earns its place when there's a genuine decision to centralize — not automatically for every interface-implementation pair.
You now know all three Factory variants, when each earns its complexity, and how your DI container has been quietly acting as a factory for your entire object graph all along.
dotnetmadeeasy.com — Learn C# and .NET, the right way.