Overview
The Factory Pattern is a fundamental member of the Creational Design Patterns family in software development. It provides a powerful abstraction for object creation, allowing developers to instantiate objects without tightly coupling the code to specific classes. By separating concerns, software architecture becomes more flexible, maintainable, and scalable.
Thanks to enhanced language features such as primary constructors, records, and pattern matching, implementing the Factory Pattern in C# 13 is even easier and more expressive. As a result, boilerplate code is reduced and modern development practices are promoted.
In this article, you'll learn what the Factory Pattern is, why and when it's useful, how to implement it cleanly using C# 13, and the best practices to follow. Lastly, we'll walk through a real-world use case to solidify your understanding.
What Is the Factory Pattern?
According to the Gang of Four (GoF), the Factory Pattern encapsulates the logic of object creation.
“Define an interface for creating an object, but let subclasses decide which class to instantiate.”
Instead of creating instances of concrete classes directly in your code, you delegate this responsibility to a factory. This abstraction allows you to switch between implementations without changing your client's code.
Problem It Solves
There are several key concerns when designing software that are addressed by the Factory Pattern:
- Reduces tight coupling: Client code no longer needs to know the exact class name or constructor parameters.
- Provides support for SOLID principles:
- The Open/Closed Principle (OCP) allows you to introduce new types without changing existing factory or client logic.
- An object creation logic is separated from business logic according to the Single Responsibility Principle (SRP).
C# 13 Code Example: Clean Factory Pattern
Let's explore a minimal example using modern C# 13 features.
// Step 1: Define the interface
namespace FactoryPattern.Core.Interfaces;
public interface INotificationService
{
void Send(string message);
};
// Step 2: Implement concrete classes
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class EmailNotificationService: INotificationService
{
public void Send(string message) => Console.WriteLine($" Email: {message}");
}
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class SmsNotificationService : INotificationService
{
public void Send(string message) => Console.WriteLine($" SMS: {message}");
}
// Step 3: Create the factory
using FactoryPattern.Core.Interfaces;
using FactoryPattern.Services;
namespace FactoryPattern.Client.Factory;
public static class NotificationFactory
{
public static INotificationService Create(string type) => type.ToLower() switch
{
"email" => new EmailNotificationService(),
"sms" => new SmsNotificationService(),
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type}")
};
}
// Step 4: Use the factory in client code
using FactoryPattern.Client.Factory;
Console.WriteLine("Hello, from Ziggy Rfiq!");
var type = args.Length > 0 ? args[0] : "email";
var notificationService = NotificationFactory.Create(type);
notificationService.Send("Hello from C# 13 Factory Pattern Code Example by Ziggy Rafiq!");
C# 13 Enhancements Highlighted
- Conditional logic inside factories can be simplified with switch expressions.
- Streamlined instantiations without verbose syntax.
- There is a clear separation between the creation of notifications (NotificationFactory) and their usage (NotificationClient).
By adopting this approach, your application becomes more maintainable, testable, and extensible, and is future-proofed for push notifications and WhatsApp notifications.
Factory Pattern Components
Each of the Factory Pattern's key components plays a distinct role in decoupling the object creation process from the client code, thus enabling flexibility and scalability.
Component Breakdown
1. Product
All concrete products must follow this contract, which is defined by this base class or interface.
namespace FactoryPattern.Core.Interfaces;
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}
2. ConcreteProduct
Listed below are the specific implementations of the Product interface. Each class implements its own logic for processing payments.
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class CreditCardProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount) =>
Console.WriteLine($"Processing credit card payment: ${amount}");
}
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class PayPalProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount) =>
Console.WriteLine($"Processing PayPal payment: ${amount}");
}
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class BankTransferProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount) =>
Console.WriteLine($"Processing bank transfer payment: ${amount}");
}
3. Creator
As part of the abstraction, this class contains the Factory Method responsible for determining which ConcreteProduct to instantiate.
using FactoryPattern.Core.Interfaces;
using FactoryPattern.Services;
namespace FactoryPattern.Client.Factory;
public static class PaymentFactory
{
public static IPaymentProcessor Create(string method) => method.ToLower() switch
{
"credit" => new CreditCardProcessor(),
"paypal" => new PayPalProcessor(),
"bank" => new BankTransferProcessor(),
_ => throw new ArgumentException("Unsupported payment method", nameof(method))
};
}
4. Client
As the Client uses the factory to obtain a product instance, it remains unaware of the specific class behind the scenes.
using FactoryPattern.Client.Factory;
Console.WriteLine("Enter payment method (credit, paypal, bank):");
string method = Console.ReadLine() ?? "credit";
try
{
var processor = PaymentFactory.Create(method);
processor.ProcessPayment(99.99m);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Component
|
Role
|
Product
|
Base interface or abstract class
|
ConcreteProduct
|
Implements the product's logic
|
Creator
|
Contains the factory method logic
|
Client
|
Uses the factory to consume the product
|
In systems where object creation logic is frequently changed or expanded, this component structure encourages loose coupling, making it easier to extend and maintain.
Example Use Case: Notification System
We'll walk through a complete real-world example where the Factory Pattern is applied to a Notification System. This system can send notifications via various channels (email, SMS, etc.), and uses a factory to decouple the creation logic from the client code.
Product Interface
To begin, we need to define a common interface for all notification types. This ensures that the client can communicate with any notification service using the same contract.
namespace FactoryPattern.Core.Interfaces;
public interface INotificationService
{
void Send(string message);
};
Concrete Implementations
The next step is to implement the interface with concrete classes representing specific notification channels, such as email and SMS.
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class EmailNotificationService: INotificationService
{
public void Send(string message) => Console.WriteLine($" Email: {message}");
}
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class SmsNotificationService : INotificationService
{
public void Send(string message) => Console.WriteLine($" SMS: {message}");
}
Enum for Notification Type
In order to make the API easier to read and less error-prone, you define an enum to simplify the way clients specify what kind of notification they want.
namespace FactoryPattern.Core.Enums;
public enum NotificationType
{
Email,
Sms
}
Factory Implementation
The factory method takes in the NotificationType enum and returns an appropriate implementation of INotificationService using a switch expression (cleaner with C# 13).
using FactoryPattern.Core.Interfaces;
using FactoryPattern.Services;
namespace FactoryPattern.Client.Factory;
public static class NotificationFactory
{
public static INotificationService Create(string type) => type.ToLower() switch
{
"email" => new EmailNotificationService(),
"sms" => new SmsNotificationService(),
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type}")
};
}
Client Code (Using the Factory)
Client code simply uses the factory to get the desired service without worrying about implementation details.
using FactoryPattern.Core.Enums;
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Client.Factory;
public class NotificationClient
{
private readonly INotificationService _service;
public NotificationClient(NotificationType type)
{
_service = NotificationFactory.Create(type.ToString());
}
public void Notify(string message) => _service. Send(message);
}
Usage Example
using FactoryPattern.Client.Factory;
using FactoryPattern.Core.Enums;
var client = new NotificationClient(NotificationType.Email);
client.Notify("Welcome to Ziggy Rafiq Srvice!");
As shown in this use case, the Factory Pattern can be applied in the following ways:
- Multi-implementation object creation simplified
- Focuses on keeping the client code clean
- Provides an easy way to extend the system (e.g., PushNotification)
- Utilizes C# 13 features such as pattern matching and switch expressions
Using this pattern, you ensure that your system is scalable, maintainable, and aligned with SOLID design principles.
C# 13 Enhancements
Class Primary Constructors are one of the most exciting features of C# 13. Previously limited to records and structs, this feature now allows you to define constructor parameters directly in the class declaration, which reduces boilerplate and improves readability.
The Factory Pattern can be applied to the NotificationClient example.
using FactoryPattern.Core.Enums;
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Client.Factory;
public class NotificationClient
{
private readonly INotificationService _service;
public NotificationClient(NotificationType type)
{
_service = NotificationFactory.Create(type.ToString());
}
public void Notify(string message) => _service.Send(message);
}
After (Using Primary Constructor in C# 13)
C# 13's primary constructors allow you to define constructor parameters directly at the class declaration level and initialize members inline.
using FactoryPattern.Core.Enums;
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Client.Factory;
public class NotificationClient(NotificationType type)
{
//private readonly INotificationService _service;
//public NotificationClient(NotificationType type)
//{
// _service = NotificationFactory.Create(type.ToString());
//}
//public void Notify(string message) => _service.Send(message);
private readonly INotificationService _service =
NotificationFactory.Create(type.ToString());
public void Notify(string message) => _service.Send(message);
}
Benefits of Primary Constructors
- In simple scenarios, there is no need for explicit constructor blocks.
- The constructor intent is immediately visible at the class declaration, thereby improving readability.
- Using constructor parameters makes it easier to initialize members.
In NotificationClient, Primary Constructors are used to make the class more concise and expressive, especially when combined with the Factory Pattern.
For factory-initialized services like this one, use primary constructors when you don't need complex construction logic or dependency validation.
Best Practices
It is critical to follow best practices for implementing factories that promote maintainability, testability, and clarity in your codebase, especially with modern features in C# 13. Here are some essential guidelines to keep in mind when implementing factories.
Use Interfaces for Return Types
Make sure your factory methods return interfaces or abstract base classes (e.g., INotificationService). This ensures loose coupling between your client and your concrete implementation, making your code more testable and easier to mock.
public static INotificationService Create(NotificationType type) => ...;
// Return type is the interface, not a concrete class
Leverage switch Expressions with Pattern Matching
Switch expressions in C# 13 are clean and expressive, making them ideal for object creation in factories. Pattern matching adds clarity and reduces boilerplate code.
using FactoryPattern.Core.Enums;
using FactoryPattern.Core.Interfaces;
using FactoryPattern.Services;
namespace FactoryPattern.Client.Factory;
public static class NotificationFactory
{
//public static INotificationService Create(string type) => type.ToLower() switch
//{
// "email" => new EmailNotificationService(),
// "sms" => new SmsNotificationService(),
// _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type}")
//};
public static INotificationService Create(NotificationType type) =>
type switch
{
NotificationType.Email => new EmailNotificationService(),
NotificationType.Sms => new SmsNotificationService(),
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type}")
};
}
Centralize Object Creation
If you keep your factory logic in a dedicated class or namespace (such as NotificationFactory in the Services.Factories namespace), you'll be able to centrally locate, maintain, and extend it more easily.
namespace ZiggyApp.Services.Factories
{
public static class NotificationFactory { ... }
}
Avoid Overusing Factories
It's better to use direct instantiation or dependency injection (DI) when the creation logic is simple and used only once. Overusing factories can clutter the code and reduce clarity.
// Acceptable for simple cases
var emailNotificationService = new EmailNotificationService();
Use Factories in ASP.NET Core with DI
It is possible to integrate factories into ASP.NET Core's dependency injection system by registering services with custom factory delegates. This will keep your code clean and fully compatible with ASP.NET Core's IoC container.
services.AddTransient<INotificationService>(sp =>
NotificationFactory.Create(NotificationType.Email));
You can then inject the appropriate implementation wherever INotificationService is needed.
By following these best practices, you ensure that your Factory Pattern implementation in C# 13 is clean, maintainable, and aligned with modern development principles like SOLID, DRY, and KISS. It is easy to scale your architecture with new services and requirements if you use factories wisely.
Real-World Example: Payment Gateway Factory
In this example, we will create a Payment Gateway Factory that supports multiple providers like Stripe and PayPal, demonstrating the Factory Pattern in action. By decoupling your application from specific payment implementations, you can remain flexible and extendable at the same time.
Define the Product Interface
The first step is to define a common interface that all payment gateways must implement. By doing this, your business logic is agnostic to the actual implementation.
namespace FactoryPattern.Core.Interfaces;
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}
Create Concrete Implementations
Create specific implementations for each payment provider next.
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class CreditCardProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount) =>
Console.WriteLine($"Processing credit card payment: ${amount}");
}
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class BankTransferProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount) =>
Console.WriteLine($"Processing bank transfer payment: ${amount}");
}
using FactoryPattern.Core.Interfaces;
namespace FactoryPattern.Services;
public class PayPalProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount) =>
Console.WriteLine($"Processing PayPal payment: ${amount}");
}
Classes implement the IPaymentGateway interface and handle charging logic differently.
Enum for Payment Providers
To avoid magic strings and to improve code readability and type safety, use an enum to define the supported payment providers.
namespace FactoryPattern.Core.Enums;
public enum PaymentMethod
{
CreditCard,
PayPal,
BankTransfer
}
Build the Factory
Using C# 13's concise switch expression, implement a static factory class that returns the appropriate IPaymentGateway based on the PaymentProvider enum.
public static IPaymentProcessor Create(PaymentMethod paymentMethod) =>
paymentMethod switch
{
PaymentMethod.CreditCard => new CreditCardProcessor(),
PaymentMethod.PayPal => new PayPalProcessor(),
PaymentMethod.BankTransfer => new BankTransferProcessor(),
_ => throw new ArgumentOutOfRangeException(nameof(paymentMethod), $"Unknown payment method: {paymentMethod}")
};
Client code does not need to worry about which concrete class to use since this factory encapsulates the instantiation logic.
Using the Factory
The factory can be used in the following ways by a client:
using FactoryPattern.Client.Factory;
using FactoryPattern.Core.Enums;
try
{
var processor = PaymentFactory.Create(PaymentMethod.CreditCard);
processor.ProcessPayment(99.99m);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
The Factory Pattern in C# 13 promotes clean architecture by simplifying dependency management and simplifying dependency management. Here is an example of a Payment Gateway Factory:
- Implementations with no impact on clients
- Use SOLID principles such as Open/Closed and Dependency Inversion
- ASP.NET Core seamlessly integrates with DI
You can easily extend this pattern by adding more providers like Square or Razorpay-just implement the interface and update the factory.
Unit Testing a Factory
Your factory logic must be unit tested to ensure that the correct implementations are returned and that your application behaves as it should. We will test the NotificationFactory using xUnit, a popular .NET testing framework, without tight coupling or direct dependencies.
Goal of the Test
We want to verify that when we request a specific notification type (e.g., Email), the factory returns the correct implementation (e.g., EmailNotificationService).
Test Implementation with xUnit
In order to validate the behavior of your factory, you can write a simple unit test as follows:
using FactoryPattern.Client.Factory;
using FactoryPattern.Core.Enums;
using FactoryPattern.Services;
namespace FactoryPattern.Tests.Factory;
public class NotificationFactoryTests
{
[Fact]
public void Factory_ReturnsEmailNotificationService()
{
// Act
var service = NotificationFactory.Create(NotificationType.Email);
// Assert
Assert.IsType<EmailNotificationService>(service);
}
[Fact]
public void Factory_ReturnsSmsNotificationService()
{
var service = NotificationFactory.Create(NotificationType.Sms);
Assert.IsType<SmsNotificationService>(service);
}
[Fact]
public void Factory_ThrowsOnUnknownType()
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
NotificationFactory.Create(((NotificationType)999)));
Assert.Contains("Unknown type", ex.Message);
}
}
Explanation
- Calls NotificationFactory.Create(...) to get an instance based on the input enum.
- A check of whether the returned object is of the expected type is performed by asserting that the object is of the type expected.
Integration with Moq is optional
Despite the fact that this specific test does not require mocking (since it is just type verification), you can integrate Moq when testing consumers of the factory, especially if you want to mock the returned service behavior or inject the factory via dependency injection.
Here are some examples:
using FactoryPattern.Core.Interfaces;
using Moq;
namespace FactoryPattern.Tests.Client;
public class NotificationClientTests
{
[Fact]
public void Notify_CallsSendMethod()
{
// Arrange
var mockService = new Mock<INotificationService>();
var client = mockService.Object;
// Act
client.Send("Hello World!");
// Assert
mockService.Verify(s => s.Send("Hello World!"), Times.Once);
}
}
In order to verify that the Send method was called, follow these steps:
mockService.Verify(s => s.Send("Hello World!"), Times.Once);
Mocking libraries like Moq can be used to isolate tests and verify interactions when testing client classes that consume factory-produced services.
Using xUnit to test your factory in C# 13 ensures:
- The concrete implementations returned by your factory are correct
- The system remains loosely coupled and testable
- It is safe to refactor or extend your factory without breaking existing logic
The combination of factory testing with Moq and dependency injection leads to a robust and flexible test suite that aligns with clean architecture principles.
Suggested Folder Structure for a Notification System Using the Factory Pattern
It is crucial to structure your codebase into meaningful folders to ensure maintainability, scalability, and a clean architecture. Here is a recommended structure for implementing the Factory Pattern in a NotificationSystem using C# 13.
/NotificationSystem
In this folder you will find all the relevant components of your notification system project organized into clear, logical subfolders.
/Interfaces/
This document contains all abstraction definitions - the interfaces that drive the design.
INotificationService.cs
The INotificationService interface ensures that your service implementations remain loosely coupled and testable.
/Services/
This class contains the concrete classes that implement the interfaces.
EmailNotification.cs
SmsNotification.cs
The classes in this section implement INotificationService and encapsulate the logic for their respective notification channels.
/Factory/
The factory responsible for creating instances of services based on input is located in this folder.
NotificationFactory.cs
All instantiation logic is handled by the factory using a switch expression, keeping client code clean and focused on behavior.
/Enums/
A collection of enum declarations used throughout the system.
NotificationType.cs
A factory receives this enum as an input and defines the types of notifications (e.g., Email, SMS).
/Client/
The factory creates classes that consume its services.
NotificationClient.cs
The factory class demonstrates how to inject and use notification services abstractly, keeping business logic independent of specific implementations.
Benefits of This Structure
- A single responsibility (SRP) is assigned to each folder.
- Easy to extend with new types like PushNotification by adding to /Services and updating the factory.
- It encourages modular testing by isolating factory behavior from service logic.
- By using interfaces and factories, clean architecture promotes abstraction and loose coupling.
To follow full Clean Architecture or Onion Architecture principles, consider grouping these folders under Core, Infrastructure, or Application layers.
The modularity, clarity, and scalability of your factory-based C# 13 system are all kept by this structure.
Summary
For creating flexible, loosely coupled systems, the Factory Pattern remains a powerful tool in software design. It becomes even more concise, expressive, and powerful when paired with C# 13's latest features, such as pattern matching, switch expressions, and primary constructors.
Key Takeaways
- Maintain business logic clarity by decoupling creation from usage: By centralizing object creation in a factory, you separate concerns.
- A factory allows you to follow the Open/Closed Principle (OCP) - new behaviors can be added without modifying existing code.
- Keep factory logic elegant, readable, and easy to maintain by using switch expressions and pattern matching.
- To streamline dependency injection, use primary constructors in consumer classes like NotificationClient.
Best Practices Recap
- Design your factory and related components to do one thing well and be open to expansion, not modification, according to SRP and OCP.
- Keep factory logic clean and expressive by using switch expressions.
- Register factories or factory methods directly into your dependency injection container using ASP.NET Core DI.
- Delegate keyword instantiation to the factory rather than spreading it across the app.
With these practices and C# 13, you can build maintainable, testable, and scalable applications that evolve gracefully over time. The Factory Pattern is not just a design tool, it's a cornerstone of a clean software architecture.
Code Repository
A complete source code demonstrating Factory Pattern examples and best practices in C# 13 is available in the Ziggy Rafiq GitHub repository. You can explore, clone, and experiment with the code to deepen your understanding.