Factory Method Design Pattern in C#/.NET
Introduction
The Factory Method is one of the most classic — and most misunderstood — design patterns in the .NET universe. Like Singleton, it belongs to the creational group of patterns, helping to solve a core dilemma in object-oriented development: who is responsible for creating class instances?
If you’ve ever asked yourself how to decouple the creation of complex objects, or how to let your system grow without spreading new everywhere, the Factory Method is for you. Let’s break down how it works, its key benefits and potential drawbacks, code examples, and how to apply it in modern .NET projects with best practices.
In other words: it delegates the responsibility of object instantiation to specialized methods or factories, centralizing creation and making your code more maintainable, testable, and extensible.
Let’s break down how it works, explore its key benefits and potential drawbacks, review code examples, and see how to apply it in modern .NET projects using best practices.
Key Benefits
When your code depends on abstractions (interfaces or abstract classes), but the logic for creating concrete implementations is scattered and hard to maintain, the Factory Method helps organize and centralize this process.
✅Centralized Creation
All instantiation logic lives in one place (or a few well-defined places), making future modifications easier.
✅ Easy to Extend
Supporting new types is as simple as adding a new implementation and updating the factory.
✅ Testability
Makes it easy to mock or fake objects in tests.
✅ Decoupling
Your code relies on abstractions, not concrete implementations.
✅ Control Over Instances
Enables singletons, caching, pooling, etc.
Potential Drawbacks
While the Factory Method pattern is useful, it’s important to be aware of the downsides:
❌ More Classes/Complexity
For simple scenarios, it may be unnecessary and make your project more verbose.
❌Possible Hidden Coupling
If the factory hides dependencies the consumer should know about, maintenance can get harder.
❌ Overengineering
It’s common to see this pattern used where a simple new would suffice.
📘 Classic Example (“by the book”)
Here’s the traditional GoF implementation (not recommended for most modern systems, but great for grasping the concept):
// Abstract Product
public abstract class Document
{
public abstract void Print();
}
// Concrete Products
public class Invoice : Document
{
public override void Print() => Console.WriteLine("Printing Invoice...");
}
public class Report : Document
{
public override void Print() => Console.WriteLine("Printing Report...");
}
// Creator (factory)
public abstract class DocumentFactory
{
public abstract Document CreateDocument();
}
// Concrete Factories
public class InvoiceFactory : DocumentFactory
{
public override Document CreateDocument() => new Invoice();
}
public class ReportFactory : DocumentFactory
{
public override Document CreateDocument() => new Report();
}
// Client code
public class Client
{
public void PrintDocument(DocumentFactory factory)
{
var doc = factory.CreateDocument();
doc.Print();
}
}- Each document type (Invoice, Report) has its own factory.
- The client doesn’t know which concrete type is used; it just uses the factory.
- Limitations: Doesn’t scale well with many types, and is unnecessarily bureaucratic for day-to-day .NET code.
🌐Real-World Example
In the real world — especially in modern .NET (ASP.NET Core) — the Factory Method is most useful when combined with dependency injection. Imagine a hospital system that sends notifications to patients through different channels (Email, SMS, Push).
Interfaces and Services
// Notification interface
public interface INotificationService
{
void Send(string recipient, string message);
}
// Email notification
public class EmailNotificationService : INotificationService
{
public void Send(string recipient, string message)
{
// Email sending logic here
Console.WriteLine($"Email sent to {recipient}: {message}");
}
}
// SMS notification
public class SmsNotificationService : INotificationService
{
public void Send(string recipient, string message)
{
// SMS sending logic here
Console.WriteLine($"SMS sent to {recipient}: {message}");
}
}
// Push notification
public class PushNotificationService : INotificationService
{
public void Send(string recipient, string message)
{
// Push notification logic here
Console.WriteLine($"Push notification sent to {recipient}: {message}");
}
}The Modern Factory
// Factory interface
public interface INotificationServiceFactory
{
INotificationService Create(string channel);
}
// Factory implementation using DI
public class NotificationServiceFactory : INotificationServiceFactory
{
private readonly IServiceProvider _serviceProvider;
public NotificationServiceFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public INotificationService Create(string channel)
{
return channel switch
{
"email" => _serviceProvider.GetRequiredService<EmailNotificationService>(),
"sms" => _serviceProvider.GetRequiredService<SmsNotificationService>(),
"push" => _serviceProvider.GetRequiredService<PushNotificationService>(),
_ => throw new ArgumentException("Unknown channel type")
};
}
}Registering in the DI Container
In Startup.cs or Program.cs:
builder.Services.AddTransient<EmailNotificationService>();
builder.Services.AddTransient<SmsNotificationService>();
builder.Services.AddTransient<PushNotificationService>();
builder.Services.AddSingleton<INotificationServiceFactory, NotificationServiceFactory>();Consuming the Factory in a Controller
public class AlertController : ControllerBase
{
private readonly INotificationServiceFactory _notificationFactory;
public AlertController(INotificationServiceFactory notificationFactory)
{
_notificationFactory = notificationFactory;
}
[HttpPost("send-alert")]
public IActionResult SendAlert(string channel, string recipient, string message)
{
var notificationService = _notificationFactory.Create(channel);
notificationService.Send(recipient, message);
return Ok("Notification sent");
}
}Why is this approach modern and recommended?
- Leverages .NET DI: Promotes separation of concerns and easy mocking in tests.
- Extensible: Adding new channels is trivial.
- Centralized and clear: All instance logic is in the factory, not scattered in the codebase.
- Testable: Easy to simulate services and validate controller behavior.
When Should You Use the Factory Method?
- When your application needs to create instances of different types at runtime, especially if new implementations may be added later.
- When object creation logic is complex or infrastructure-dependent (caching, pooling, etc).
- When you want to decouple creation from usage (e.g., in controllers, services, handlers).
Note: In modern .NET projects with DI, prefer injected factories and avoid unnecessary inheritance.
Summary
The Factory Method remains a fundamental pattern for applications that need to grow and stay flexible. In today’s .NET, it’s most powerful when combined with Dependency Injection. It helps centralize, organize, and test your code — if used judiciously and with a focus on real needs.
📚 References
Gang of Four — Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)
Microsoft Docs — Dependency injection in .NET
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection
