TL;DR
- How Factory Method decouples object creation from the code that uses objects
- When to reach for Factory Method vs. a simple constructor or Abstract Factory
- Real-world .NET implementations:
ILoggerFactory,IHttpClientFactory, custom factories - 6 production bugs caused by wrong factory patterns and how to avoid them
Define an interface for creating objects — let subclasses decide which class to instantiate.
LoggerFactory.CreateLogger("Orders") or HttpClientFactory.CreateClient("github"), you're using Factory Method. The framework decides which concrete logger or HTTP client to build — you just ask for one by name. The pattern is everywhere in .NET, and once you see it, you'll spot it in every codebase you touch.
What: Imagine you're writing code that needs to create objects, but you don't want to hardcode which exact class to create. Maybe today you need a PdfExporter, but tomorrow your teammate needs an ExcelExporter. Instead of littering your code with new PdfExporter(), you define a method — a "factory method" — that creates the object. Then, different subclassesA class that inherits from another class. In Factory Method, each subclass overrides the factory method to return a different type of product. The parent class defines the "what" (create a document exporter), each subclass defines the "which" (PDF, Excel, CSV). can override that method to return different types. The code that uses the object never knows which concrete class was created — it just works with the shared interface.
When: Use Factory Method when a class can't anticipate which objects it needs to create, or when you want subclasses to specify the objects. It's perfect when you're building a frameworkA reusable set of classes that provides a skeleton for applications. Frameworks use Factory Method heavily because the framework author can't know what specific objects YOUR application needs — they just define the "shape" and let you fill in the details. or library where the user (another developer) should be able to plug in their own types without modifying your code.
In C# / .NET: The classic approach uses an abstract class with a virtual or abstract method that subclasses override. In modern .NET, you'll often see it done through DI registrationDependency Injection registration: telling the DI container "when someone asks for IExporter, give them PdfExporter." The container becomes the factory — you register the mapping once in Program.cs, and the right type gets created automatically wherever it's needed. or factory delegates. .NET itself uses this pattern everywhere: ILoggerFactory.CreateLogger(), IHttpClientFactory.CreateClient(), WebApplication.CreateBuilder().
Quick Code:
Prerequisites
Interfaces & Polymorphism — Factory Method is all about returning different objects through a shared contract. Your code says "give me an
INotification" and it might get an email sender, an SMS sender, or a push notification sender — it doesn't know which. If the idea of "programming to an interface" makes sense to you, you're halfway there.Inheritance & Method Overriding — The classic version of this pattern uses a base class with a virtual or abstract methodA virtual method has a default implementation that subclasses CAN override. An abstract method has NO implementation — subclasses MUST override it. In Factory Method, the "create" method is usually abstract, forcing each subclass to provide its own creation logic. that subclasses override. Each subclass returns a different product. If you understand how a child class can replace a parent's method with its own version, you'll follow the examples easily.
Open/Closed Principle (OCP) — One of the biggest wins of Factory Method is that you can add new product types without modifying existing code. You write a new subclass, and everything else stays untouched. That's the Open/Closed Principle"Software entities should be open for extension, but closed for modification." In practice: adding a PushNotificationCreator shouldn't require editing EmailNotificationCreator, SmsNotificationCreator, or any code that uses them. New behavior = new code, not changed code. in action — and Factory Method is one of the cleanest ways to achieve it.
Dependency Injection basics — In modern .NET, factory methods often work hand-in-hand with the DI container. Understanding how to register and resolve servicesRegistration: telling the DI container "when someone needs INotification, build an EmailNotification." Resolution: the container automatically creates and injects the right object when a class asks for INotification in its constructor. This replaces manual "new" calls. in
Program.cswill help you see how Factory Method fits into real ASP.NET Core applications.
Analogies
You walk into a restaurant and order "a burger." You don't walk into the kitchen, pick the buns, season the patty, and fire up the grill yourself. You just say what you want. The kitchen decides how to make it — which recipe to follow, which chef handles it, which ingredients to use. A burger at a fast-food joint and a burger at a gourmet bistro are both "burgers," but the kitchen (the creator) behind each restaurant produces a completely different product.
The key insight: you (the client code) never touch the creation details. You just ask for "a burger" through the menu (the interface). If the restaurant wants to add a new item — say, a veggie burger — they update the kitchen, not the menu. Existing customers don't even notice the change.
| Real World | What it represents | In code |
|---|---|---|
| Restaurant chain | The base class that defines the process | Creator (abstract class) |
| Specific restaurant (e.g., gourmet kitchen) | A subclass that decides what to build | ConcreteCreator |
| "Make me a burger" | The factory method call | CreateBurger() |
| The actual burger you receive | The concrete object created | ConcreteProduct |
| "Burger" as a concept (any style) | The shared interface all products follow | IBurger (Product interface) |
| Adding a veggie burger option | New subclass, zero changes elsewhere | New VeggieKitchen : Kitchen |
The diagram above shows the flow: the customer (your code) asks for a burger. The abstract Kitchen decides the overall process. Each concrete kitchen — fast food, gourmet, or veggie — overrides the creation step to produce its specific burger. Adding a new kitchen type doesn't change anything for the customer or the other kitchens.
-
Car Dealership — You walk into a Toyota dealership and say "I need a sedan." You don't go into the factory and assemble the car yourself — the dealership handles all of that. A Toyota dealership gives you a Camry. Walk into a BMW dealership with the same request? You get a 3 Series. Both are sedans, both have four wheels and a steering wheel, but the dealership (the Creator) decided which specific car (the Product) to build. You just asked for "a sedan" — the concrete type was the dealership's decision, not yours.
-
Document Templates — You click "New Document" in an app. A word processor creates a
.docx. A spreadsheet app creates a.xlsx. A presentation app creates a.pptx. The button is identical — "create new document" — but the application behind it decides which specific document type to produce. The user never specifies the file format; the app's factory method handles that. And if someone builds a new app for diagrams? It creates.drawiofiles — same button, new product, zero changes to existing apps. -
Logistics — A shipping company needs to create "transport." The road logistics branch creates Trucks — each with a load capacity, fuel type, and route plan. The sea logistics branch creates Ships — with container slots, port schedules, and customs paperwork. The planning code just says
CreateTransport()and gets back something that canDeliver(cargo). It works identically whether goods travel by land, sea, or (someday) drone. New transport mode? New branch class, same planning code.
Core Concept Diagram
Factory Method has four roles that work together. The Creator is a base class (or interface) that declares the factory method — it says "there will be a method called CreateProduct(), but I'm not going to implement it." Each ConcreteCreator (a subclass) overrides that method and returns a specific product. The Product is the interface that all created objects share, and each ConcreteProduct is a specific implementation. The beauty is: the Creator can use the product without ever knowing its concrete type.
Here's what the UML shows: the Creator (top-left) has an abstract CreateProduct() method and a concrete DoWork() method that calls the factory method. The Creator doesn't know what it's creating — it just trusts that CreateProduct() will return something that implements IProduct. Each ConcreteCreator overrides the factory method and returns a specific ConcreteProduct. The yellow dashed arrow shows the "returns new" relationship — ConcreteCreatorA creates ConcreteProductA.
Sequence Diagram — Runtime Flow
Let's trace what happens at runtime when client code calls DoWork() on a ConcreteCreator.
Walk through it step by step: (1) Client calls DoWork() on the Creator. (2) Inside DoWork(), the Creator calls its own CreateProduct() — but because the ConcreteCreator overrides this method, the subclass's version runs. (3) The ConcreteCreator creates a specific product. (4) That product is returned to the Creator. (5) The Creator calls Execute() on the product — it never knows the concrete type. The Creator works with the product purely through the IProduct interface.
ILoggerFactory.CreateLogger() is exactly this: the factory has shared logic (formatting, filtering by log level), and each logging provider (Console, Serilog, NLog) overrides the creation part.
Code Implementations
Let's see Factory Method in action across three different real-world scenarios. Each one uses the same structural idea — "let the subclass decide what to create" — but the domain and style vary so you can see how flexible this pattern is.
Imagine you're building a system that sends alerts — sometimes by email, sometimes by SMS, sometimes by push notification. The logic around sending (retry on failure, log the attempt, validate the recipient) is always the same. The only thing that changes is how the notification is built. That's a perfect Factory Method scenario.
Notice the pattern: the NotifyWithRetryAsync method has all the shared logic — retry with exponential backoff, logging, error handling. It calls CreateNotification() without knowing or caring what comes back. Each ConcreteCreator just says "for me, the notification is an email" or "for me, it's an SMS." Adding a SlackNotificationCreator tomorrow means writing one new class — the retry logic, logging, and everything else stays exactly the same.
Many apps need to export data in different formats — PDF, Excel, CSV. The "export" workflow is always the same: gather the data, transform it into the target format, and stream it to the user. The only part that changes is which format gets produced. Factory Method makes each format pluggable.
The GenerateReportAsync method handles fetching data, validation, logging, and returning the file — all shared. Each subclass only provides the exporter. When the team needs a Word document exporter, they write DocxExporter and DocxReportService. The core report pipeline doesn't change at all.
Payment gateways are a classic example: every provider (Stripe, PayPal, Square) follows the same high-level flow — validate, charge, log — but the actual API calls are completely different. Factory Method keeps the workflow stable while letting each gateway plug in its own integration.
The ProcessAsync workflow — charge, update order, log — is identical for every gateway. Each ConcreteCreator just wires up the right gateway. When the team adds Square support, it's one SquareGateway class and one SquareCheckout class. Nothing in the existing payment code changes.
Execution Flow
This diagram traces what happens when the Notification example runs end-to-end, from DI resolution through the factory method call and into the concrete product.
The flow is straightforward: the DI container creates the right ConcreteCreator (based on what's registered in Program.cs). When the shared workflow calls CreateNotification(), the overridden version in the ConcreteCreator runs, returning the specific product. From that point on, the Creator interacts with the product only through the INotification interface — it has no idea it's dealing with email, SMS, or anything else.
Junior vs Senior
Build a document export service that can generate reports in multiple formats (PDF, Excel, CSV). The format is chosen by the user at request time. The service should be easy to extend with new formats (e.g., Word) without modifying existing code.
How a Junior Thinks
"I'll just use a switch statementThe classic procedural approach: check the format string, then create the right exporter inside a big method. Seems clean at first, but every new format means adding another case to the switch — touching tested code and risking merge conflicts. to pick the right format. It's all in one file, easy to understand."
Problems
Adding a Word exporter means opening this method and adding another case branch. You're modifying tested, working code. In a team of 10, everyone editing the same switch statement means constant merge conflicts.
PdfRenderer is created with new inside the method — you can't mock it. Testing the PDF path requires an actual PDF library. Testing CSV accidentally runs through PDF validation too. Each format is tangled with the others.
Each case handles everything from scratch — no shared logging, no shared validation, no shared error handling. When the team decides "all exports must log the file size," someone has to add that logging code to every single case branch.
How a Senior Thinks
"Each export format is a separate concern with its own dependencies. I'll define a Product interface (IDocumentExporter), implement each format as its own class, and use a Creator that handles the shared workflow — fetching data, logging, error handling. The factory method is the only part that changes per format. Adding Word? One new class, one DI registration."
Design Decisions
PdfExporter gets its own unit tests with a mocked IPdfRenderer. ExcelExporter has separate tests — no PDF library needed. The Creator is testable too: mock the repository and verify the workflow without any real exporter.
Data fetching, validation, logging, and file naming are all in the Creator — written once, tested once. The factory method is the only customization point. "All exports must log the file size" is a one-line change in one place.
Instead of manually mapping format strings to creators, Keyed ServicesA .NET 8 feature: register multiple implementations of the same type with different string keys. Then resolve by key at runtime: sp.GetRequiredKeyedService<ReportService>("pdf"). No factory class needed — the DI container IS the factory. handle it. The DI container resolves the right ReportService subclass by format key — no switch statement, no manual factory.
Evolution & History
Factory Method didn't appear out of thin air. It evolved alongside the languages and frameworks that use it. Understanding that journey helps you see why the pattern looks different in a modern .NET 8 codebase versus a 2002-era .NET 1.0 project — and why both approaches are still the same fundamental idea underneath.
In 1994, the "Gang of Four" published Design Patterns. Their Factory Method chapter described a world of C++ and Smalltalk — languages where creating the right object type was genuinely painful. The idea was elegant: instead of calling new ConcreteClass() directly, you call a method that subclasses override to return whatever type they want. The calling code never changes.
Back then, this was revolutionary. Most codebases had massive switch statements deciding which class to instantiate. Factory Method replaced those with polymorphismThe ability of different classes to respond to the same method call in different ways. Instead of a switch statement asking "what type?", you let each class define its own behavior. — each subclass knows what to create, so no central switch is needed.
Notice how Application.NewDocument() doesn't know or care what kind of document it's working with. That's the entire power of the pattern — the workflow is fixed, but the object type is pluggable.
When .NET arrived, C# brought the same ideas into a managed runtime. Early .NET factories looked almost identical to the GoF book — abstract base classes with virtual or abstract methods. But .NET 2.0 added genericsA language feature that lets you write code that works with any type, decided at compile time. Instead of writing separate factories for each product, you can write one generic factory: Factory<T>., which eliminated a lot of boilerplate.
Before generics, you'd cast everything to object and back — risky and ugly. After generics, the factory could be IFactory<T> and return strongly-typed products with zero casts.
LINQ and lambda expressions changed everything. Suddenly you didn't need a whole class just to say "here's how to create an object." A Func<T> delegate could serve as a tiny, inline factory — no inheritance hierarchy needed.
This was a pivotal shift. For simple creation logic — where the factory method is just "call a constructor with these arguments" — a full class hierarchy felt like overkill. A one-line lambda does the same job with less ceremony. The pattern is still Factory Method (something decides what to create), but the mechanism shrank from a class to a function.
The downside? When creation logic is complex (needs configuration, logging, validation), a delegate gets unwieldy fast. That's when you still want the class-based approach.
.NET Core made dependency injectionA technique where objects receive their dependencies from the outside (usually a "container") instead of creating them internally. Instead of a class calling new EmailService(), the DI container hands it an IEmailService automatically. a first-class citizen. The built-in IServiceProvider is essentially a giant factory — you register types at startup, and the container creates the right concrete class whenever someone asks for an interface.
This didn't kill Factory Method — it absorbed it. Instead of writing your own factory classes, you register creation logic with the DI container, and it handles instantiation, lifetime management, and disposal. The pattern is still there, just hiding inside the framework.
The newest addition: keyed servicesA .NET 8 feature that lets you register multiple implementations of the same interface, each identified by a unique key (string, enum, etc.). The DI container returns the right implementation based on the key you request — no manual factory class needed.. Before .NET 8, if you had multiple implementations of the same interface and needed to pick one by name, you had to write manual factory logic (like the Func<string, INotification> workaround above). Keyed services make this a first-class DI feature — register by key, resolve by key, done.
This is the most concise the Factory Method pattern has ever been in .NET. The [FromKeyedServices] attribute on a constructor parameter tells the DI container "give me the implementation registered under this key." No factory class, no delegate, no switch statement — the container handles it all.
The evolution is clear: from verbose abstract class hierarchies (1994) to a single attribute on a constructor parameter (2023). The idea never changed — "let something else decide which concrete type to create." The syntax got dramatically simpler.
Factory Method in .NET Core
.NET is practically a museum of Factory Methods. Once you know the pattern, you'll spot it in every namespace — logging, HTTP, DI scoping, options, database contexts. These aren't academic examples; they're production code you'll call every day.
Let's walk through the most important Factory Method implementations that ship with .NET itself. For each one, we'll see what it does, why it uses Factory Method (instead of just new), and how to use it in your own code.
If you've ever written logger.LogInformation("...") in a .NET app, you've benefited from Factory Method. ILoggerFactory has a single method: CreateLogger(string categoryName). You ask for a logger by name, and the factory decides how to build it — which sinks to attach (console, file, Seq), what minimum log level, what formatting rules.
Why Factory Method here? Because the logger you need depends on configuration, not code. The same CreateLogger("Orders") call might produce a console-only logger in development and a structured-JSON-to-Elasticsearch logger in production. The calling code is identical in both environments — only the factory registration changes.
Raw new HttpClient() is a famously bad idea in .NET — it leads to socket exhaustionWhen you create and dispose HttpClient instances too frequently, the underlying TCP connections linger in TIME_WAIT state. After enough instances, your OS runs out of available sockets and new HTTP requests start failing — even though you "disposed" the client properly. under load. IHttpClientFactory solves this by managing a pool of HttpMessageHandler instances. But it's also a Factory Method: you call CreateClient("github") and get back an HttpClient pre-configured with the right base URL, headers, timeout, and retry policy.
Why Factory Method? Because each external API needs different configuration. Your GitHub client needs an OAuth token and a specific base URL. Your payment gateway client needs client certificates and custom retry logic. The factory lets you define these configurations once at startup and stamp out correctly-configured clients on demand.
The very first line of every modern .NET app is a factory method: WebApplication.CreateBuilder(args). It creates a WebApplicationBuilder pre-configured with logging, configuration sources (appsettings.json, environment variables, command-line args), and the DI container.
Why Factory Method? Because the builder needs to set up dozens of defaults — Kestrel server config, JSON settings, environment detection — and those defaults differ between web apps, worker services, and minimal APIs. A constructor couldn't handle this complexity cleanly. The static factory method encapsulates all that setup and returns a ready-to-customize builder.
In .NET's DI system, scoped servicesServices that live for the duration of a "scope" — typically one HTTP request. A scoped DbContext means each request gets its own database connection and transaction. Different requests never share scoped instances. exist for the lifetime of a scope (usually one HTTP request). But sometimes you need to create a scope manually — in a background service, a message handler, or a hosted service. That's where IServiceScopeFactory.CreateScope() comes in.
Why Factory Method? Because each scope is an isolated container that tracks its own disposable objects. The factory ensures proper setup and teardown — when the scope is disposed, all scoped services within it get disposed too. You can't just new a scope; the factory needs to wire it into the DI container's lifetime management.
The Options pattern.NET's built-in pattern for reading typed configuration. Instead of reading raw strings from appsettings.json, you define a class (e.g., SmtpOptions) and .NET automatically maps the JSON properties into that class. Clean, typed, and validatable. binds configuration sections to strongly-typed classes. Under the hood, IOptionsFactory<T>.Create() is a factory method that reads configuration, applies validation, and returns a fully-constructed options object.
Why Factory Method? Because options construction is surprisingly complex: read from multiple sources (JSON, env vars, Azure Key Vault), apply named options, run IValidateOptions<T> validators, and apply post-configuration. All of this happens inside the factory, invisible to the consumer who just injects IOptions<SmtpOptions>.
Entity Framework Core's DbContext is not thread-safe — you can't share one across concurrent operations. In a Blazor ServerA hosting model where the Blazor UI runs on the server and communicates with the browser over SignalR. Because the connection is long-lived (not request-scoped), the normal "one DbContext per request" approach doesn't work — you need to create and dispose contexts manually. app or a background service, where there's no HTTP request scope, you need to create DbContext instances on demand. IDbContextFactory<T> does exactly that.
Why Factory Method? Because creating a DbContext involves connection string lookup, provider selection (SQL Server, Postgres, SQLite), migration check, and interceptor setup. The factory encapsulates all of this. You call CreateDbContext(), get a fully configured context, use it, and dispose it. Simple.
When To Use / When Not To
Factory Method is powerful, but it's not always the right tool. Using it where a simple new would suffice adds needless complexity. Using new where a factory is needed creates rigid, untestable code. Here's how to make the right call.
Decision Flowchart
Not sure? Walk through this flowchart. Start at the top and follow the path.
Comparisons
Factory Method lives in a neighborhood of patterns that all deal with object creation and behavioral delegation. If you've ever wondered "should I use Factory Method or Abstract Factory?" or "isn't this just Strategy?" — this section untangles those overlaps.
Factory Method vs Abstract Factory
When to pick which: If you're creating one kind of thing (notifications, exporters, loggers), Factory Method is enough. If you're creating sets of things that must match (a UI theme where buttons, checkboxes, and menus all need to be "dark mode" together), that's Abstract Factory. Think of Factory Method as a single creation point, Abstract Factory as a coordinated creation suite.
Factory Method vs Strategy
When to pick which: Factory Method answers "which object do I create?" Strategy answers "which algorithm do I run?" Sometimes they overlap — a factory might create strategy objects. But if the object itself is the point (it has state, multiple methods, a lifecycle), use Factory Method. If you just need to swap one chunk of behavior, Strategy is leaner.
Factory Method vs Simple Factory (Static Method)
When to pick which: Simple Factory (a static method like Notification.Create("email")) is fine for small projects where the product list rarely changes. Factory Method shines when you need extensibility — new product types added by new classes, not by editing a switch statement. If you find yourself constantly editing a switch to add new types, it's time to graduate to Factory Method.
Factory Method vs Builder
When to pick which: Factory Method is "give me an object" — one call, one result. Builder is "let me configure this object piece by piece" — multiple steps, then finalize. If the complexity is in which type to create, use Factory Method. If the complexity is in how to configure a single type (many optional parameters, conditional steps), use Builder.
Visual: Factory Method vs Abstract Factory
SOLID Connections
Factory Method is one of those rare patterns that supports all five SOLID principles. That's not a coincidence — the pattern was designed to make code flexible and maintainable, which is exactly what SOLID principles aim for. Let's see how each connection works specifically for Factory Method.
| Principle | Verdict | How Factory Method Connects |
|---|---|---|
| SRPSingle Responsibility Principle: A class should have only one reason to change. If your class both processes orders AND decides which payment gateway to use, it has two responsibilities — and two reasons to break. | Supports | Creation logic lives in the factory (one responsibility), while the business workflow lives in the creator's template method (a different responsibility). The ReportService doesn't know how to build a PdfExporter — it just calls CreateExporter() and uses the result. If PDF generation changes, only the PdfReportService subclass changes. |
| OCPOpen/Closed Principle: Software should be open for extension but closed for modification. You should be able to add new behavior without editing existing, tested code. | Supports | This is Factory Method's headline feature. Adding a new product type (say, MarkdownExporter) means creating a new MarkdownReportService subclass. You never touch ReportService, PdfReportService, or any existing code. The existing code is closed for modification; the system is open for extension via new subclasses. |
| LSPLiskov Substitution Principle: Any subclass should be usable wherever its parent class is expected, without breaking behavior. If code works with a base Creator, it must work with ANY ConcreteCreator. | Supports | Every ConcreteCreator must be substitutable for the base Creator. The caller works with the Creator type and never knows which subclass is active. If PdfReportService behaves differently than ExcelReportService in ways the caller doesn't expect, LSP is violated — and the pattern breaks. Factory Method requires LSP to function correctly. |
| ISPInterface Segregation Principle: Clients shouldn't be forced to depend on methods they don't use. Keep interfaces small and focused — one purpose per interface. | Supports | The Product interface (INotification, IDocumentExporter) should be focused and lean. Factory Method encourages this: if the interface is bloated, some concrete products will have dummy methods they don't need — a clear ISP violation. The pattern nudges you toward small, cohesive product interfaces. |
| DIPDependency Inversion Principle: High-level modules should depend on abstractions, not concrete classes. Don't import PdfExporter directly — depend on IDocumentExporter and let the factory provide the concrete type. | Supports | The Creator depends on the IProduct interface, not any concrete product. The calling code depends on the Creator abstraction, not any concrete creator. All dependencies point toward abstractions. This is why Factory Method makes testing so easy — you can substitute any implementation without the caller knowing. |
The takeaway: Factory Method doesn't just happen to align with SOLID — it was designed around these principles. If you find your Factory Method implementation violating one of these (e.g., a bloated product interface, or modifying existing creators to add new types), something is off with the implementation, not the pattern.
Bug Case Studies
Factory Method bugs are sneaky. The pattern itself is simple, but the ways it can go wrong in production are subtle — null returns, memory leaks, circular dependencies, missing registrations, and race conditions. Each bug below comes from real-world patterns we've seen in .NET codebases. Study them so you don't repeat them.
NullReferenceException in production after a new "push" notification channel was added to the database by the marketing team. The factory's switch statement didn't have a case for "push," so it hit the default branch — which returned null. The null propagated through three method calls before finally crashing in an unrelated-looking line. The error message pointed to notification.SendAsync(), not the factory — making it look like a sending bug, not a creation bug. Root cause: the factory silently returned null instead of failing loudly. Impact: 2,400 push notifications silently dropped over 3 hours before anyone noticed.
Time to diagnose: 45 minutes. The stack trace pointed to SendAsync(), not the factory. The team searched the sending logic for 30 minutes before someone added a null-check upstream and traced it back to the factory.
The default branch returns null instead of throwing. The caller doesn't check for null because it expects the factory to always return a valid object. When "push" arrives, null silently flows through until something tries to call a method on it.
Two fixes: (1) add the missing case, and (2) make the default branch throw with a descriptive message. The return type is now non-nullable (INotification, not INotification?), so the compiler enforces that every path returns a real object.
T?) or have default: return null. Also look for // TODO: add new types here comments — they mean someone knew the switch was incomplete and left a trap for the next developer.
OutOfMemoryException after running for about 6 hours. The factory was creating a new PdfExporter on every request — and each PdfExporter loaded a 12 MB font library into memory on construction. At 50 requests per minute, that's 600 MB per hour of unreferenced font data waiting for garbage collection. Root cause: the factory treated stateless, reusable objects as if they needed fresh instances. Impact: service restarted 3 times in one day before the team found the leak.
Time to diagnose: 2 hours. Memory profiling showed thousands of identical PdfExporter instances on the heap, each holding the same font data. The trail led straight to the factory's Create() method.
Every call to Create("pdf") allocates a brand new PdfExporter that loads fonts, templates, and rendering config from scratch. Since the exporter is stateless (same output for same input), there's zero reason to create a new one each time.
Two approaches: cache inside the factory with ConcurrentDictionary, or register the products as singletons in DI. The key insight: if a product is stateless (no per-request data stored in fields), it can be safely shared across all callers.
new in a factory, ask: "Is this product stateless?" If yes, cache it. If it holds per-request state, create fresh — but make sure the expensive parts (font loading, template parsing) are shared or lazy-loaded.
PdfExporter instances that are all equivalent, the factory is over-creating. Also search for new HeavyObject() inside factory methods — if the object is heavy and stateless, it should be cached.
StackOverflowException. The DI container tried to create OrderService, which needed INotificationFactory, which needed IOrderService to look up order details for notification templates. Neither could be created without the other — an infinite construction loop. Root cause: the factory depended on a service that depended on the factory. Impact: deployment failed completely; the app couldn't start. Rolled back to previous version while the team untangled the dependency graph.
Time to diagnose: 20 minutes. The StackOverflowException stack trace showed the same two constructors alternating endlessly. Once you see that pattern, you know it's circular DI.
The factory constructor takes IOrderService, and OrderService takes the factory. The DI container can't resolve either without the other — classic circular dependency.
The fix: the factory depends on infrastructure (SMTP client, SMS gateway), not on business services. The order data that notifications need is passed as method parameters — no circular dependency.
InvalidOperationException: No service for type 'StripeGateway' has been registered in production on the first Stripe payment attempt. The developer had added the StripeGateway class and factory case but forgot to register it in Program.cs. Root cause: the factory resolved types from the DI container, but the new type wasn't registered. Impact: all Stripe payments failed for 40 minutes until a hotfix was deployed.
Time to diagnose: 5 minutes (the error message was clear). Time to deploy the fix: 35 minutes (CI/CD pipeline). Total downtime: 40 minutes for a one-line fix.
The factory code is correct — it asks the DI container for StripeGateway. But the type was never registered. This compiles fine and passes unit tests (which mock the factory). It only fails at runtime when the real DI container is asked for a type it doesn't know about.
The real fix isn't just adding the registration — it's adding a startup validation that checks all factory dependencies are registered before the app starts serving traffic. This turns a runtime surprise into a boot-time failure with a clear error message.
GetRequiredService or GetService calls inside factory methods. For each resolved type, verify it has a corresponding AddTransient/AddScoped/AddSingleton registration in Program.cs. Better yet, write a test that boots the real DI container and resolves every factory product.
Analyzer instance, and both tried to store it. One thread's instance was silently lost, and — worse — the analyzer had a one-time initialization step (loading ML model weights) that wasn't idempotent. The second initialization corrupted shared state, causing incorrect analysis results for 12 hours before QA caught the data drift. Root cause: non-thread-safe lazy initialization in a singleton factory. Impact: subtle data corruption — the most dangerous kind of bug because it doesn't crash, just produces wrong results.
Time to diagnose: 12+ hours. No crashes, no exceptions. QA noticed analysis scores were slightly off compared to the previous week. The team eventually traced it to two concurrent initializations of the ML model weights, where the second one partially overwrote the first.
The check-then-create sequence is not atomic. Two threads can both see _cached is null, both create an instance, and the second _cached = assignment overwrites the first. If LoadModelWeights() has side effects on shared state, it gets called twice on different objects — causing subtle corruption.
Lazy<T> with ExecutionAndPublication mode ensures that only one thread ever executes the creation logic. All other threads that call .Value concurrently will block and wait for the first thread to finish, then reuse the same instance. For keyed caches, wrap each value in Lazy<T> inside a ConcurrentDictionary.
Lazy<T> or ConcurrentDictionary<K, Lazy<V>> for thread-safe initialization. Never use a bare if (cache == null) check in concurrent code — it's a race condition waiting to happen.
if (_field is null) or if (_field == null) patterns inside singleton or static classes. If the field is assigned inside that if block without locking, you have a race condition. Replace with Lazy<T> — it's simpler and correct by construction.
Pitfalls & Anti-Patterns
Factory Method is one of the most intuitive patterns, but that ease of understanding leads to some sneaky mistakes. Most of these pitfalls don't cause compile errors — they cause design rot: code that works today but becomes a nightmare to extend tomorrow. Let's walk through the seven most common mistakes, why they happen, and how to fix them.
Mistake: You create one giant factory class that can produce every type of object your app needs — notifications, payments, reports, users, everything. Sounds convenient at first. After six months you have a 1,200-line factory with 30 switch cases.
Why This Happens: It starts innocently. You add a factory for notifications. Then someone needs a payment factory, and instead of creating a separate class, they add another method to the existing factory. The factory grows because it’s easy to extend an existing file than think about design.
Fix: One factory = one product family. If the products are unrelated (notifications, payments, reports), they get separate factories. Each factory has one reason to change, which is exactly what the Single Responsibility Principle asks for.
Mistake: When the factory gets a type it doesn't recognize, it quietly returns null. The caller doesn't check for null, and you get a NullReferenceException three method calls later — far from the actual bug.
Why This Happens: Developers think returning null is "safe" because it doesn't crash immediately. But null is not safe — it's a delayed crash. The real cause (unknown type) and the symptom (NRE) are in completely different places, making debugging a nightmare.
Fix: Throw immediately with a descriptive message that names the bad input and lists valid options. The stack trace points exactly to the factory, not to some random downstream method. If you genuinely need a "maybe" return, use a TryCreate pattern that returns bool with an out parameter — making the caller explicitly handle the failure case.
Mistake: Your factory returns INotifier, which is great. But then the caller immediately casts it to EmailNotifier to access some email-specific property like .SmtpServer. You've just defeated the entire purpose of the factory — the caller is now tightly coupled to the concrete type again.
Why This Happens: The concrete type has extra capabilities that aren't on the interface. Instead of enriching the interface, developers take the "quick fix" of casting. Over time, half the codebase is littered with (EmailNotifier) casts that break as soon as you swap implementations.
Fix: If callers need extra capabilities, extend the interface hierarchy. Use is pattern matching for optional features, not hard casts. The concrete type should stay hidden behind the interface boundary.
Mistake: You create a DateTimeFactory, a StringBuilderFactory, a ListFactory — factories for types that have perfectly good constructors and never vary. Every new in the codebase gets wrapped in a factory "just in case."
Why This Happens: Pattern enthusiasm. You just learned Factory Method and it clicks, so you apply it everywhere. The result is indirection with no benefit: extra files, extra interfaces, extra DI registrations — all wrapping a simple new List<T>().
Fix: Use Factory Method when you have at least two product variants (or strong reason to expect them soon), when creation involves real logic (configuration, pooling, validation), or when the type is chosen at runtime. For simple value types and stable utility classes, new is the right answer.
Mistake: Your NotificationFactory calls a ChannelFactory, which calls a TransportFactory, which calls a ConnectionFactory. Four layers of factories to send an email. Nobody knows where objects actually get created, and debugging requires stepping through factories all the way down.
Why This Happens: Each layer of "abstraction" felt reasonable in isolation. A factory for notifications makes sense. A factory for channels makes sense. But chaining them creates a Rube Goldberg machine where every creation goes through four levels of indirection.
Fix: Let the DI container compose deep dependency chains. Your factory should make one decision: which concrete type to create. The container handles wiring the rest. If you need nested creation, consider whether it's really one factory that takes a few parameters, not a chain of four factories.
Mistake: Your factory creates objects that hold database connections, file handles, or HTTP connections (they implement IDisposable). The caller uses the object but never disposes it. Connections leak, handles exhaust, and the app dies under load.
Why This Happens: When you call new DbConnection() directly, you naturally wrap it in a using block because you see the type. When a factory returns IDataProcessor, you don't see that the concrete type holds a connection — the abstraction hides the disposal requirement.
Fix: If any concrete product implements IDisposable, make the product interface extend IDisposable (or IAsyncDisposable). This signals to every caller: "you must dispose what you get." The using keyword then kicks in naturally.
Mistake: You inject IServiceProvider into every class and call sp.GetService<INotifier>() wherever you need a notifier. You call it a "factory" because it creates objects, but it's actually the
Why This Happens: IServiceProvider is easy to inject and can resolve anything. It feels like a universal factory. But it hides your class's real dependencies behind a generic "give me anything" interface, making dependencies invisible and testing painful.
Fix: Inject specific factory interfaces, not IServiceProvider. The only places where IServiceProvider is acceptable are: (1) inside the factory implementation itself, (2) in middleware/framework plumbing, and (3) in IHostedService that needs to create scopes. Application code should never touch the container directly.
Testing Strategies
Factory Method is one of the most testable patterns out there, because it's built on interfaces. The factory returns an interface, the caller depends on an interface, and interfaces are trivially mockable. But there are four distinct testing angles you should cover, and each one catches a different category of bugs.
The simplest test: call the factory with a known input and assert the returned type is correct. This catches typos in switch expressions, missing registrations, and wrong mappings. It's fast, deterministic, and should be your first line of defense.
Notice we test three things: (1) known inputs return the right type, (2) unknown inputs throw the right exception, and (3) the returned object actually implements the interface. These three checks catch 90% of factory bugs.
Sometimes you don't want to test the factory itself — you want to test the code that uses the factory. In that case, you create a fake factory that returns a mock product, isolating the consumer from the real creation logic.
This is where Factory Method really shines for testability. Because the consumer depends on INotifierFactory (an interface), you can swap in a test double that returns whatever you want — a fake that records calls, a mock that throws, or a stub that returns a canned response.
Unit tests verify logic in isolation. But what if the factory is wired to the DI container and the registration is wrong? Integration tests spin up a real IServiceProvider and verify that the factory can actually resolve its dependencies end-to-end.
This catches the "Missing Registration" bug from S12 — where the factory code is correct but the DI wiring is incomplete. You're testing the composition root, not the factory logic.
Factory Method has a dual role: it creates objects and it often lives inside a Creator class that has shared workflow logic (the
The key insight: create a TestableCreator subclass that overrides the factory method to return a fake product. Now you can test the workflow logic in isolation, without any real products being created.
Performance Considerations
Let’s address the elephant in the room: Factory Method adds a virtual method call and an interface dispatch. Should you worry about that? Almost certainly not. A virtual method call costs about 2–5 nanoseconds on modern hardware — the same time it takes light to travel one meter. Unless your factory is called millions of times per second in a tight loop, the pattern’s overhead is invisible next to real work like database queries (5–50 ms) or HTTP calls (50–500 ms).
That said, certain factory implementations can introduce performance problems. Not from the pattern itself, but from what you do inside the factory. Here’s a breakdown of the common costs and how to manage them.
| Concern | Typical Cost | When It Matters | Mitigation |
|---|---|---|---|
| Virtual method dispatch | 2–5 ns | Never in practice | None needed — JIT often devirtualizes |
| Object allocation per call | ~20–50 ns + GC pressure | Hot loops creating thousands of objects | Object pooling (ObjectPool<T>) |
| Reflection-based type resolution | ~500–2000 ns | Factory resolves types via Activator.CreateInstance |
Cache compiled delegates; use FrozenDictionary |
| Dictionary lookup for type mapping | ~10–30 ns (O(1)) | Watch for GC from captured closures | Use static lambdas or FrozenDictionary |
| Lazy initialization (first call) | ~100–300 ns (once) | First request latency spike | Warm up in IHostedService at startup |
| DI container resolution | ~50–200 ns | Transient services with deep dependency graphs | FrozenDictionary for .NET 8+ immutable lookups |
The takeaway from the chart: virtual dispatch and dictionary lookups are noise. Allocation matters only at high volume. DI resolution is fast for shallow graphs. Reflection is the only one that can genuinely hurt in hot paths — and it's avoidable.
Here's a
Typical results on an Intel i7-12700K (.NET 8):
| Method | Mean | Allocated | Notes |
|---|---|---|---|
| DirectNew | ~3 ns | 24 B | Baseline — just allocation |
| SwitchFactory | ~5 ns | 24 B | Near-baseline, JIT optimizes the switch |
| DictionaryFactory | ~22 ns | 24 B | Hash + closure invoke |
| FrozenDictFactory | ~12 ns | 24 B | Faster lookup, static lambda = no closure GC |
| ReflectionFactory | ~800 ns | 48 B | 160x slower — avoid in hot paths |
FrozenDictionary with static lambdas. For everything else (99% of factory usage), don't optimize — clarity beats nanoseconds.
Interview Pitch
When an interviewer asks "Tell me about Factory Method," you have about 90 seconds before they either move on or follow up. Here's a script that covers all the bases without sounding rehearsed.
90-Second Pitch:
Opening (What it is): "Factory Method is a creational pattern where you define a method for creating objects, but let subclasses or implementations decide which concrete class to instantiate. Instead of hard-coding new SpecificClass() everywhere, you call a method that returns an interface — and the actual type is chosen behind the scenes."
Core (How it works): "You have two abstractions: a Creator that declares the factory method, and a Product interface for what gets created. Each ConcreteCreator overrides the factory method to return a different ConcreteProduct. The caller works with the Creator and Product interfaces — never the concrete types. This means you can add new product types without changing any existing code."
Example (.NET): "In .NET, ILoggerFactory.CreateLogger() is a textbook Factory Method. You call it with a category name, and it returns an ILogger. Whether that logger writes to the console, a file, or Application Insights depends on how the factory was configured at startup — your code never knows or cares. IHttpClientFactory.CreateClient() follows the exact same pattern for HTTP clients."
When to use: "I reach for Factory Method when the type of object I need is decided at runtime, when creation involves non-trivial logic like configuration or pooling, or when I want to follow the Open/Closed Principle so adding new types doesn't require modifying existing classes."
Close (SOLID connection): "It supports Open/Closed because new types are new classes, not changes. It supports Dependency Inversion because high-level code depends on interfaces, not concrete types. And it makes testing easy because you can swap the factory with a fake in tests."
Q&As
29 questions organized by difficulty. Click any question to reveal the answer. Each one starts with a "Think First" prompt — try to answer before peeking. The hard questions are the ones that come up in senior/staff-level interviews.
Easy Foundations (Q1—Q6)
Factory Method is a design where you ask for an object through a method, and something else decides which specific class to create. You work with the result through an interface — you don't know (or care) what concrete class was instantiated behind the scenes.
Think of it like this: you call factory.Create("email") and get back an INotifier. Whether that's an EmailNotifier, a SendGridNotifier, or a MockNotifier depends on how the factory is configured. Your code just calls notifier.Send() and it works.
The formal GoF definition: "Define an interface for creating an object, but let subclasses decide which class to instantiate." In modern .NET, "subclasses" often becomes "the DI container" or "a delegate," but the core idea is identical: the caller doesn't pick the type.
new?new EmailNotifier() when you need to swap to SendGridNotifier?When you write new EmailNotifier(), you've hard-coded the concrete type. Every file that has that line is now tightly coupled to EmailNotifier. If you switch to SendGrid, you find-and-replace across the entire codebase.
With Factory Method, the concrete type is decided in one place (the factory). Every other file just calls factory.Create() and gets an INotifier. Switching from EmailNotifier to SendGridNotifier means changing one factory class — zero changes everywhere else.
The trade-off: new is simpler. If the type never changes and creation is trivial, new is the right choice. Factory Method earns its keep when you have multiple implementations, runtime type selection, or complex creation logic.
Create...() method and get back an interface.1. ILoggerFactory.CreateLogger(categoryName) — returns ILogger. The factory decides whether it's a console logger, file logger, or Application Insights logger based on configuration.
2. IHttpClientFactory.CreateClient(name) — returns HttpClient. The factory configures the client with the right base URL, headers, retry policies, and handler pipeline based on the registered name.
3. IDbContextFactory<T>.CreateDbContext() — returns a fresh DbContext instance. Essential in Blazor Server and background services where a single DbContext can't be shared across threads.
new ConcreteClass() and you need to change the concrete class?It solves the tight coupling between creation code and consuming code. Without Factory Method, every place that creates an object knows exactly which concrete class to instantiate. That means:
- Adding a new variant requires modifying every creation site
- Switching implementations is a find-and-replace nightmare
- Testing requires the real concrete class (can't swap in fakes)
- You violate the Open/Closed Principle every time a new type appears
Factory Method centralizes the "which type?" decision into one place, so the rest of the codebase works through interfaces and never knows which concrete class it's using.
IStringBuilderFactory? (Hint: see Pitfall 4.)Don't use Factory Method when:
- Only one implementation exists and you don't expect more. A factory for
StringBuilderis overhead with zero benefit. - Creation is trivial — no configuration, no pooling, no conditional logic. Just
new Thing(). - The type is known at compile time and never varies. No need for runtime indirection.
- .NET 8 keyed services already solve your problem. If you just need named resolution without workflow logic, use
[FromKeyedServices("name")]— no factory class needed.
The rule of thumb: if you can delete the factory and replace all usages with new ConcreteType() without any downside, the factory was unnecessary.
1. Product (interface) — the shared contract for the objects being created. Example: INotifier.
2. ConcreteProduct (class) — a specific implementation. Example: EmailNotifier, SmsNotifier.
3. Creator (abstract class/interface) — declares the factory method. May also contain shared workflow logic. Example: NotificationService with an abstract CreateNotifier().
4. ConcreteCreator (class) — overrides the factory method to return a specific ConcreteProduct. Example: EmailNotificationService that returns new EmailNotifier().
Medium Applied Knowledge (Q7—Q16)
Factory Method is about one creation point: a single method that returns one product type. Subclasses override that method to change which concrete type is returned. It uses inheritance.
Abstract Factory is about families of related products: a factory interface with multiple Create...() methods that all produce related types (e.g., CreateButton(), CreateCheckbox(), CreateTextBox() for a UI toolkit). It uses composition — you inject the factory object.
In practice, Abstract Factory is often implemented with Factory Methods. Each Create...() method in the Abstract Factory is a Factory Method.
DI and Factory Method are complementary, not competing:
- DI gives you a pre-built object at construction time. You register
INotifier→EmailNotifierand the container injects it when the class is constructed. - Factory Method creates objects on demand at runtime, often based on runtime data. "Give me the right notifier for this specific order's notification preference."
Use DI when the type is known at configuration time. Use Factory Method when the type depends on runtime input. In modern .NET, factories themselves are typically registered in DI: services.AddSingleton<INotifierFactory, NotifierFactory>().
Technically, no — a static method can't be overridden by subclasses, which is central to the GoF Factory Method pattern. If the method is static, you have a Simple Factory (also called Static Factory Method), which is useful but isn't the GoF pattern.
That said, C# has a convention of static factory methods on the type itself: TimeSpan.FromSeconds(30), Task.FromResult(value), IPAddress.Parse("..."). These are excellent API design but serve a different purpose: they're alternative constructors, not polymorphic creation points.
Use static factory methods for convenience constructors on a single type. Use the GoF Factory Method when you need polymorphic creation (subclass decides the type).
Three options, ranked best to worst:
- Throw
ArgumentExceptionwith a message that names the bad input AND lists valid options. This is the default choice. The stack trace points right at the factory. TryCreatepattern — returnbool+outparameter, likeint.TryParse(). Use when the caller legitimately doesn't know if the type is supported.- Return a default/null-object (a do-nothing implementation). Only use if "do nothing" is genuinely acceptable behavior.
Never return null. It moves the crash from the factory (easy to debug) to somewhere downstream (nightmare to debug).
The factory method itself is typically thread-safe because it usually holds no mutable state — it just creates a new object and returns it. A pure switch expression with new calls is inherently thread-safe.
Thread-safety problems appear when factories cache instances:
- Mutable dictionary as cache — two threads calling
Create("email")simultaneously can corrupt aDictionary<K,V>. UseConcurrentDictionary. - Lazy initialization without locks — two threads see the cache as empty, both create an instance, one overwrites the other. Use
Lazy<T>orConcurrentDictionary.GetOrAdd(). - Singleton products that aren't thread-safe — if the factory caches and returns the same instance to all callers, that instance must be thread-safe.
Rule: if your factory creates a new object each time, it's thread-safe. If it caches, use ConcurrentDictionary<K, Lazy<V>>.
INotifierFactory, what can you inject in tests?Three approaches (see S14 for full code):
- Fake factory — create a test implementation of
INotifierFactorythat returns a fake/mock product. No mocking framework needed. - Moq / NSubstitute —
var mockFactory = new Mock<INotifierFactory>(); mockFactory.Setup(f => f.Create(It.IsAny<string>())).Returns(fakeNotifier); - Testable subclass — if using the GoF inheritance approach, create a
TestableCreatorthat overrides the factory method to return a fake.
The key point: because the consumer depends on an interface (not a concrete factory), swapping in test doubles is trivial. This is one of Factory Method's biggest practical benefits.
Absolutely — and it's a very common combination. The factory creates the strategy at runtime based on some input, and the context uses the strategy without knowing which one it got.
Factory Method handles which strategy to use. Strategy handles how the algorithm works. Clean separation of concerns.
Simple Factory is a static method (or a class with a non-virtual method) that uses a switch to pick the concrete type. It's not a GoF pattern — it's just a helper. To add a new type, you modify the switch.
Factory Method uses a virtual/abstract method that subclasses override. Adding a new type means adding a new subclass — existing code stays untouched (Open/Closed Principle).
Simple Factory is fine for small, stable sets of types. Factory Method earns its keep when types change frequently or the creation decision needs to be customizable per use case.
Open/Closed says: you should be able to add new behavior without changing existing code.
With Factory Method, adding a new product type means:
- Write a new
ConcreteProductclass (implements the Product interface) - Write a new
ConcreteCreatorclass (overrides the factory method) - Register it in the DI container (one line in
Program.cs)
None of the existing Creator, Product, or consumer code is modified. The new type is purely additive. Compare this to a switch statement where every new type requires editing the switch — that's a modification, which OCP tries to avoid.
.NET 8 Keyed Services let you register multiple implementations of the same interface, each with a unique key:
This eliminates the factory class when all you need is named resolution with no workflow logic. But Factory Method is still valuable when:
- The key comes from runtime data (not known at injection time)
- The Creator has shared workflow around the creation
- You need custom creation logic (pooling, validation, configuration)
Think of keyed services as a simple case optimization. They cover 70% of factory use cases with zero boilerplate.
Hard Expert Level (Q17—Q29)
new an object that lives in another service. What does "creating" a product mean across service boundaries?In microservices, the factory doesn't create the real object — it creates a proxy (client) that communicates with the remote service via HTTP/gRPC. The Product interface stays the same (IPaymentGateway), but the ConcreteProduct is a client wrapper:
- Factory picks the right HTTP client based on provider name ("stripe", "paypal")
- Product is a typed HTTP client that wraps API calls behind the
IPaymentGatewayinterface - Registration uses
IHttpClientFactoryfor connection pooling and resilience - Discovery may use a service registry (Consul, Kubernetes DNS) to find the right endpoint
The pattern is identical — only the transport changes from in-process method calls to network calls.
Activator.CreateInstance() uses reflection. How much slower is that than new?Reflection-based factories (using Activator.CreateInstance or Type.GetConstructors()) are ~100-300x slower than direct new:
new EmailNotifier(): ~3 ns — compiler-generated IL, JIT-optimizedActivator.CreateInstance(type): ~800-2000 ns — type lookup, constructor lookup, boxing, security checks- Compiled lambda via
Expression.Compile(): ~5-10 ns after initial compile — first call is expensive (~50 ms), subsequent calls matchnew
If you must use reflection (e.g., plugin systems that discover types at startup), compile a delegate at startup and cache it. The FrozenDictionary<string, Func<IProduct>> pattern gives you O(1) lookup + direct-call speed at runtime, paying the reflection cost only once during boot.
The classic Factory Method returns a synchronous object. But what if creation requires async work — like connecting to a database, fetching configuration from a vault, or warming up a cache?
Key design decisions:
- Return
Task<IProduct>, notIProduct— the caller knows creation is async - Accept
CancellationToken— creation might be slow, callers need a way to abort - Make the product
IAsyncDisposablesoawait usingworks - Consider
ValueTask<IProduct>if the common path is cached (avoids allocation)
Factory Method is usually presented as a SOLID champion, but it can violate SOLID when misapplied:
- SRP violation: The Creator has two responsibilities — its own workflow logic and the factory method. If the factory decision is complex, extract it into a separate factory class.
- LSP violation: If a ConcreteCreator's factory method returns a product that doesn't fully satisfy the Product interface contract (e.g., throws
NotSupportedExceptionfor some methods), consumers that expect the full interface will break. - ISP violation: If the Product interface is fat (20 methods) and some ConcreteProducts only implement half, the factory is handing callers an interface that's too broad. Split the interface.
- OCP violation: A
switch-based factory that requires modification for each new type violates OCP. True OCP compliance requires the inheritance-based or plugin-based approach.
The pattern enables SOLID but doesn't guarantee it. You still need to apply the principles consciously.
Reflection runs once at startup, building a FrozenDictionary that provides O(1) lookups at runtime with zero reflection cost. Adding a new notifier type means adding a class with the [FactoryKey] attribute — no factory code changes needed. True open/closed.
Func<T> — when is a delegate enough?Func<string, INotifier> is a one-line factory. When does that stop being enough?A Func<T> delegate is a factory — one that fits in a single line. Use it when:
- The creation logic is simple (no configuration, no caching, no validation)
- You don't need to unit test the factory independently
- There's no shared workflow around the creation
Switch to a full Factory Method class when:
- Creation involves configuration, caching, pooling, or validation
- The factory needs its own dependencies (injected via DI)
- You want to test the factory in isolation
- The Creator has workflow logic around the creation (template method aspect)
- You need a discoverable, named abstraction in your codebase (an interface, not an opaque
Func)
Func<T> is the quick version. Factory Method is the robust version. Start with Func<T> and upgrade when you feel the pain.
In systems that run for years, you often need to support multiple versions of the same product simultaneously. The factory becomes version-aware:
Key design choices: default to the latest version, allow explicit version requests for legacy clients, and use the factory as the single point where version routing is decided.
It depends on whether the factory failure is recoverable or fatal:
- Fatal (missing DI registration, configuration error): Let it crash. Fail fast at startup with a clear error. Don't catch and swallow.
- Recoverable (user requests unsupported type): Return a meaningful error response (HTTP 400), log a warning, don't crash the process.
- Degradable (preferred type unavailable, fallback exists): Use a chain-of-responsibility factory that tries the preferred type, then falls back to a default.
The anti-pattern is catching the exception and returning a "default" product silently. The caller thinks it got what it asked for, but it's using a completely different implementation. This causes subtle, hard-to-diagnose bugs.
Yes, but with nuances:
Records work perfectly with Factory Method. Records can implement interfaces, so the factory returns IPaymentResult and the concrete type is a record. Bonus: records are immutable, so there are no concerns about thread safety or state mutation after creation.
Value types (structs) are trickier. The factory returns an interface, but structs implementing interfaces get boxed (allocated on the heap, negating the struct's performance benefit). If you're using Factory Method with structs for performance, consider returning a concrete struct type via generics: T Create<T>() where T : struct, IProduct.
- Constructor: The type is known, creation is simple, few parameters. Use
new. - Factory Method: The type varies at runtime. You need polymorphic creation.
- Builder: The type is known, but configuration is complex — many optional parameters, step-by-step setup.
They can combine: a factory that returns a builder (factory picks the product family, builder configures the instance), or a builder that uses a factory internally for component creation.
IServiceProvider and resolves anything looks like a factory but acts like a Service Locator. Where's the line?The line is: does the class resolve ONE specific product type, or can it resolve ANYTHING?
- Factory:
INotifierFactory.Create(string type) → INotifier. It creates exactly one thing: notifiers. It's focused and its purpose is clear from the interface. - Service Locator:
IServiceProvider.GetService(Type type) → object. It can resolve anything. Injecting it hides what a class actually depends on.
A factory may use IServiceProvider internally (that's fine — it's an implementation detail). But a factory should never expose IServiceProvider to its consumers. The factory interface should be narrow and typed: INotifier Create(string type), not object GetService(Type type).
Wrap the factory in a decorator that adds cross-cutting behavior:
This is the Decorator pattern applied to factories. You can stack them: logging → caching → validation → actual creation. Each layer is independently testable.
Factory Method is evolving, not disappearing:
- Keyed Services (.NET 8+) eliminate simple factory classes for named resolution. ~70% of "factory" use cases become one-liner DI registrations.
- Source Generators can auto-generate factory code from attributes at compile time, eliminating reflection entirely while keeping the discovery pattern from Q21.
- Native AOT limits reflection (no
Activator.CreateInstancein trimmed builds), pushing factories toward compile-time resolution via source generators or manual registration. - Primary Constructors (C# 12) make writing ConcreteCreators shorter — less boilerplate, same pattern.
- Static abstract interface members (C# 11) enable type-level factory contracts:
static abstract IProduct Create()on the Product interface itself.
The core idea — "something decides which type to create so the caller doesn't have to" — is permanent. Only the mechanics simplify with each C# release.
Practice Exercises
Five hands-on exercises, ordered from easy to hard. Try to solve each one before peeking at the solution. The timer is optional but helps build interview-speed coding muscle.
Build a ShapeFactory that creates Circle, Square, and Triangle. Each shape implements IShape with a Draw() method that returns a string like "Drawing circle with radius 5". The factory takes a shape name (string) and returns IShape.
- Define
IShapewithstring Draw() - Create three classes:
Circle,Square,Triangle - The factory should use a
switchexpression - Throw
ArgumentExceptionfor unknown shapes - Write a test that verifies all three shapes are created correctly
Build a notification system with three channels: Email, SMS, and Push. Each implements INotifier with Task SendAsync(string to, string message). Create a factory, register everything in the DI container, and write an integration test that verifies all three channels resolve correctly.
- Define
INotifierandINotifierFactory - Each notifier can just log the message (no real sending needed)
- Register all three notifiers as transient in DI
- The factory takes
IServiceProviderand usesGetRequiredService - Integration test: build a real
ServiceProviderand verify each type resolves
You have a DocumentParser class with a giant switch statement that parses PDF, DOCX, and CSV files differently. Refactor it to use Factory Method so adding a new format (e.g., XLSX) requires zero changes to existing code.
- Extract the interface:
IDocumentParserwithDocument Parse(Stream content) - Create three parsers:
PdfParser,DocxParser,CsvParser - Build
IDocumentParserFactorythat takes a file extension - The factory maps ".pdf" → PdfParser, ".docx" → DocxParser, ".csv" → CsvParser
- Verify that adding XLSX means adding just a new class + one DI line
Build an async factory that creates database connections with health checks. The factory should support PostgreSQL and SQL Server, open the connection asynchronously, run a health check query (SELECT 1), and return the connection only if the health check passes. Include a CancellationToken and proper IAsyncDisposable support.
- Define
IDatabaseConnection : IAsyncDisposablewithQueryAsync<T> - The factory method signature:
Task<IDatabaseConnection> CreateAsync(string dbType, CancellationToken ct) - Open the connection and run
SELECT 1before returning - If health check fails, dispose the connection and throw
- Use
await usingin the caller
Build a factory that discovers INotifier implementations at startup by scanning assemblies for classes decorated with [FactoryKey("...")]. The factory should compile the type map into a FrozenDictionary so runtime lookups have zero reflection overhead. Write an extension method AddNotifiers() for IServiceCollection that does the scanning and registration.
- Create a
[FactoryKey(string)]attribute - Decorate your notifier classes with
[FactoryKey("email")], etc. - In
AddNotifiers(), useAssembly.GetTypes()to find allINotifierclasses with the attribute - Build a
Dictionary<string, Type>and freeze it with.ToFrozenDictionary() - Register both the types (transient) and the factory (singleton) in DI
Cheat Sheet
Pin this to your desk. Six cards covering everything you need to recall about Factory Method in 60 seconds flat.
ILoggerFactory.CreateLogger()IHttpClientFactory.CreateClient()IDbContextFactory<T>.CreateDbContext()null — throw insteadIDisposable on productsDeep Dive
The textbook stops at "subclass overrides a factory method." Real systems push further. These three advanced techniques show how Factory Method adapts when your requirements outgrow the basics.
Instead of one-subclass-per-product, pass a key and let a single factory decide.
Classic Factory Method creates one ConcreteCreator per product type. That works when you have 3–5 types, but with 20+ it becomes a class explosion. The parameterized variant accepts a key (a string, enum, or type token) and resolves the right product from a map — one factory class, unlimited products.
.NET 8 introduced keyed services that do exactly this at the framework level. You register each implementation under a key, and the DI container becomes your parameterized factory:
The beauty is that the factory (DI container) never needs to change. Adding a new channel means adding a class and a registration line. The NotificationService doesn't even know how many channels exist — it just asks for one by key.
The Creator's workflow IS a template method. The factory method is just one step in it.
Look closely at the Creator class in any real Factory Method implementation and you'll spot something familiar: a fixed sequence of steps where one step is overridable. That's the Template Method pattern.
In fact, the GoF book itself says Factory Method is often called by Template Methods. The Creator defines the workflow (validate → create → configure → log), and the CreateProduct() step is the factory method that subclasses override:
This is why Factory Method is so powerful in practice. It's not just about creating objects — it's about plugging a creation decision into a larger, shared workflow. The Template Method gives you the workflow reuse; the Factory Method gives you the flexibility point within it.
When creating a product requires I/O, the factory method returns a Task<T>.
Sometimes building a product isn't instant. Maybe you need to fetch configuration from a database, call an external API for credentials, or establish a network connection. A synchronous factory would block the calling thread — a disaster in ASP.NET Core where every blocked thread is a request that can't be served.
The fix is straightforward: make the factory method return Task<IProduct> instead of IProduct:
Key design rules for async factories:
- Always accept a
CancellationToken— callers need a way to bail out if creation takes too long - If creation fails after partial setup (connection opened but schema invalid), dispose the partial object before rethrowing
- Never use
.Resultor.Wait()on async factories — that defeats the entire purpose and risks deadlocks
Mini-Project
Build a plugin-based notification system from scratch — and see how each refactoring step makes the codebase more flexible.
Most developers start here. You need to send notifications via email, SMS, and Slack, so you write one class with a switch statement. It works — until the fourth channel arrives and you realize every change means editing this same file.
- Open/Closed violation — every new channel means editing this file
- Untestable — real SMTP/HTTP calls are baked in, can't mock them
- No shared workflow — retry logic, logging, and validation must be duplicated per case
- Disposable leak —
HttpClientandSmtpClientcreated per call, never disposed
Extract a INotifier interface, create one class per channel, and wire them through DI with keyed services. The factory method resolves the right implementation by key — no switch, no concrete types in caller code.
- Open/Closed — new channel = new class + one DI registration line
- Testable — mock
INotifierto test the workflow without real HTTP - Shared workflow — validation, logging, and retry live in one place
Program.cs to add a registration line for each new channel. Can we eliminate even that?The final evolution. Instead of manually registering each notifier, we scan assemblies at startup to find every class implementing INotifier that has a [FactoryKey] attribute. Adding a new channel means dropping a new class file (or even a new DLL) — zero changes to any existing code, including DI registration.
- Zero coupling — the factory never references any concrete notifier by name
- Plugin-capable — pass external assemblies to
AddNotifiers()for third-party plugins - Fast at runtime — reflection runs once at startup;
FrozenDictionarygives O(1) lookups - Discoverable —
typeMap.Keystells you exactly which channels are available
Migration Guide
You have a 500-line switch statement that creates objects. Here's how to safely refactor it into a Factory Method — one step at a time, with tests passing at every stage.
Find the common operations across all branches of the switch statement. These become the interface. Don't add anything that not all branches share — keep it lean.
Take the logic from each case branch and move it into its own class that implements the interface. One class per case. Keep the original switch working alongside the new classes during this transition.
Register each concrete product in the DI container using keyed services (or a dictionary-based factory if you're on older .NET). Then replace the switch statement with a single factory resolution call.
PdfExporter), those callers will break — fix them to use the IExporter interface.Search the entire codebase for any remaining references to concrete product types. Replace them with the interface. Then delete the old switch statement class (if it still exists) and remove the using statements.
Code Review Checklist
Use this checklist every time you review (or write) Factory Method code. Ten checks that catch the most common mistakes before they reach production.
| # | Check | Why It Matters | Red Flag |
|---|---|---|---|
| 1 | Factory returns an interface, not a concrete type | Callers should never know the concrete type — that's the whole point of the pattern | public PdfExporter Create() instead of IExporter |
| 2 | No null return — throw on unknown key |
Null returns cause NullReferenceException far from the factory, making debugging painful |
default: return null; in a switch |
| 3 | Products implement IDisposable if they hold resources |
Connections, streams, and HTTP clients leak without proper disposal | Product wraps HttpClient but isn't disposable |
| 4 | Factory is registered with correct DI lifetime | Singleton factory + transient product = products live forever (captive dependency) | Singleton factory creating scoped DbContexts |
| 5 | Factory method accepts CancellationToken (if async) |
Without it, callers can't cancel long-running creation (DB connections, HTTP calls) | Task<IProduct> CreateAsync() with no cancellation parameter |
| 6 | Error messages include available keys | When resolution fails, the error should guide the developer to valid options | throw new Exception("Unknown type") with no list of valid types |
| 7 | No concrete product references outside the factory | If callers reference PdfExporter directly, the factory is being bypassed |
using MyApp.Exporters; in a controller file |
| 8 | Factory doesn't grow beyond ~10 products | A factory with 20+ products is a god factory — split by domain (exporters, notifiers, etc.) | Single ServiceFactory creating every type in the system |
| 9 | Unit tests cover each product type independently | Each concrete product should have its own test class with focused assertions | One mega test file testing all products through the factory |
| 10 | DI integration test verifies wiring | Runtime resolution failures (missing registrations) aren't caught by unit tests | No test calling provider.GetRequiredService<IFactory>() |
grep -rn "new PdfExporter\|new CsvExporter\|new ExcelExporter" --include="*.cs" src/ to find concrete product usage outside factory code. Any hits in controller, service, or handler files are red flags that the factory is being bypassed.