Mastering Design Patterns in C# and ASP.NET Core
Part 2.3 – Abstract Factory Design Pattern
Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Creational Design Pattern
Difficulty: ⭐⭐⭐☆☆ (Intermediate)
Prerequisites: OOP Concepts, Interfaces, Factory Method Pattern, Dependency Injection
Table of Contents
Introduction
What is the Abstract Factory Pattern?
Why Do We Need It?
Problem Statement
Real-World Analogy
Factory Method vs Abstract Factory
Creating Families of Related Objects
UML Class Diagram
Components of the Pattern
Complete C# Console Application
ASP.NET Core Example
Real-World Use Cases
Advantages
Disadvantages
Best Practices
Common Mistakes
Interview Questions
Summary
Introduction
As applications grow, we often need to create multiple related objects that are designed to work together.
Consider a cross-platform UI application. The application supports:
Windows
macOS
Each platform has its own set of controls:
Windows
Windows Button
Windows TextBox
Windows CheckBox
macOS
Mac Button
Mac TextBox
Mac CheckBox
If client code creates these objects directly, it's easy to accidentally mix incompatible components—for example, using a Windows Button with a Mac TextBox.
The Abstract Factory Pattern solves this problem by ensuring that related objects are created together from the same product family.
What is the Abstract Factory Pattern?
Definition
The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Unlike the Factory Method Pattern, which creates one product, Abstract Factory creates multiple related products that are intended to work together.
Why Do We Need the Abstract Factory Pattern?
Suppose you're building an e-commerce application that integrates with multiple payment providers.
Each provider requires:
Payment Service
Refund Service
Invoice Service
For Stripe:
StripePayment
StripeRefund
StripeInvoice
For PayPal:
PayPalPayment
PayPalRefund
PayPalInvoice
Without an Abstract Factory, the application could accidentally combine a Stripe payment service with a PayPal refund service.
The Abstract Factory guarantees that all related services come from the same provider.
Problem Statement
Without the Abstract Factory Pattern:
IPayment payment = new StripePayment();
IRefund refund = new PaypalRefund();
This creates an inconsistent combination.
The application should either use all Stripe services or all PayPal services—not a mixture.
Real-World Analogy
Imagine buying furniture for your home.
You purchase a Living Room Set.
It includes:
Sofa
Chair
Coffee Table
All pieces belong to the same design family.
You don't usually buy:
Victorian Sofa
Modern Coffee Table
Industrial Chair
because they may not match.
Similarly, the Abstract Factory creates a complete, compatible family of related objects.
Factory Method vs Abstract Factory
| Feature | Factory Method | Abstract Factory |
|---|---|---|
| Creates | One product | A family of related products |
| Pattern Type | Creational | Creational |
| Uses Inheritance | Yes | Often combines interfaces and composition; implementations may use Factory Methods internally |
| Complexity | Lower | Higher |
| Best For | Single object creation | Coordinated creation of related objects |
| Example | Create a Payment Service | Create Payment, Refund, and Invoice services together |
Creating Families of Related Objects
Imagine two payment providers.
Stripe Family
StripePayment
StripeRefund
StripeInvoice
PayPal Family
PayPalPayment
PayPalRefund
PayPalInvoice
The Abstract Factory ensures that selecting the Stripe factory creates only Stripe services, while selecting the PayPal factory creates only PayPal services.
UML Class Diagram
+-------------------------------+
| IPaymentProviderFactory |
+-------------------------------+
| + CreatePayment() |
| + CreateRefund() |
| + CreateInvoice() |
+---------------^---------------+
|
+-----------------+-----------------+
| |
+-------------------------+ +-------------------------+
| StripeFactory | | PayPalFactory |
+-------------------------+ +-------------------------+
| CreatePayment() | | CreatePayment() |
| CreateRefund() | | CreateRefund() |
| CreateInvoice() | | CreateInvoice() |
+-------------------------+ +-------------------------+
Components of the Pattern
Product Interfaces
public interface IPayment
{
void Pay(decimal amount);
}
public interface IRefund
{
void Refund(decimal amount);
}
public interface IInvoice
{
void GenerateInvoice();
}
Stripe Products
public class StripePayment : IPayment
{
public void Pay(decimal amount)
{
Console.WriteLine($"Stripe paid ₹{amount}");
}
}
public class StripeRefund : IRefund
{
public void Refund(decimal amount)
{
Console.WriteLine($"Stripe refunded ₹{amount}");
}
}
public class StripeInvoice : IInvoice
{
public void GenerateInvoice()
{
Console.WriteLine("Stripe invoice generated.");
}
}
PayPal Products
public class PaypalPayment : IPayment
{
public void Pay(decimal amount)
{
Console.WriteLine($"PayPal paid ₹{amount}");
}
}
public class PaypalRefund : IRefund
{
public void Refund(decimal amount)
{
Console.WriteLine($"PayPal refunded ₹{amount}");
}
}
public class PaypalInvoice : IInvoice
{
public void GenerateInvoice()
{
Console.WriteLine("PayPal invoice generated.");
}
}
Abstract Factory
public interface IPaymentProviderFactory
{
IPayment CreatePayment();
IRefund CreateRefund();
IInvoice CreateInvoice();
}
Concrete Factories
Stripe Factory
public class StripeFactory : IPaymentProviderFactory
{
public IPayment CreatePayment() => new StripePayment();
public IRefund CreateRefund() => new StripeRefund();
public IInvoice CreateInvoice() => new StripeInvoice();
}
PayPal Factory
public class PaypalFactory : IPaymentProviderFactory
{
public IPayment CreatePayment() => new PaypalPayment();
public IRefund CreateRefund() => new PaypalRefund();
public IInvoice CreateInvoice() => new PaypalInvoice();
}
Complete C# Console Application
class Program
{
static void Main()
{
IPaymentProviderFactory factory = new StripeFactory();
IPayment payment = factory.CreatePayment();
IRefund refund = factory.CreateRefund();
IInvoice invoice = factory.CreateInvoice();
payment.Pay(2500);
refund.Refund(500);
invoice.GenerateInvoice();
}
}
Output
Stripe paid ₹2500
Stripe refunded ₹500
Stripe invoice generated.
To switch providers, change only the factory:
IPaymentProviderFactory factory = new PaypalFactory();
The client code remains unchanged.
ASP.NET Core Example
The Abstract Factory Pattern integrates naturally with Dependency Injection.
Register Factories
builder.Services.AddScoped<StripeFactory>();
builder.Services.AddScoped<PaypalFactory>();
You can also register the abstract factory based on configuration:
builder.Services.AddScoped<IPaymentProviderFactory>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var provider = config["PaymentProvider"];
return provider == "Stripe"
? new StripeFactory()
: new PaypalFactory();
});
Inject into a Controller
public class PaymentController : ControllerBase
{
private readonly IPaymentProviderFactory _factory;
public PaymentController(IPaymentProviderFactory factory)
{
_factory = factory;
}
[HttpPost]
public IActionResult Process()
{
var payment = _factory.CreatePayment();
payment.Pay(5000);
return Ok();
}
}
Changing the provider in configuration changes the family of services without modifying controller code.
Real-World Use Cases
The Abstract Factory Pattern is useful when you need to create sets of related objects.
Examples include:
Cross-platform UI toolkits (Windows, macOS, Linux)
Payment provider integrations (Stripe, PayPal, Razorpay)
Database providers (SQL Server, PostgreSQL, Oracle)
Cloud providers (Azure, AWS, Google Cloud)
Logging frameworks with related components
Theme engines (Light Theme, Dark Theme)
Document generation suites (PDF, Excel, Word)
Advantages
Ensures related objects are compatible.
Promotes consistency across product families.
Supports the Open/Closed Principle.
Reduces coupling by programming to interfaces.
Simplifies switching between implementations.
Improves maintainability in large systems.
Disadvantages
Introduces more interfaces and classes.
Can increase complexity for small applications.
Adding a new product type (e.g., a new service in every family) requires updating the abstract factory interface and all concrete factories.
Requires careful design to avoid unnecessary abstraction.
Best Practices
Use Abstract Factory when products naturally belong together.
Keep product interfaces cohesive and focused.
Use Dependency Injection to supply factories.
Avoid embedding business logic inside factories.
Prefer configuration-driven selection of concrete factories in ASP.NET Core.
Common Mistakes
Mixing Product Families
Avoid creating products from different families in the same workflow unless your design explicitly supports it.
Using Abstract Factory for a Single Product
If only one object needs to be created, the Factory Method Pattern is usually simpler.
Overengineering
For small applications with one implementation, introducing an Abstract Factory may add unnecessary complexity.
Interview Questions
1. What is the Abstract Factory Pattern?
It is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes.
2. When should you use the Abstract Factory Pattern?
When you need to create groups of related objects that must work together, such as UI controls for different operating systems or services for different payment providers.
3. How is Abstract Factory different from Factory Method?
Factory Method creates a single product through a factory method. Abstract Factory creates multiple related products through a common factory interface.
4. Does Abstract Factory use Factory Method?
Often, yes. Many implementations use one or more factory methods inside concrete factories to create each product, although the exact implementation can vary.
5. What SOLID principles does Abstract Factory support?
Open/Closed Principle (OCP): New product families can be added by introducing new concrete factories.
Dependency Inversion Principle (DIP): Client code depends on abstract factory and product interfaces rather than concrete implementations.
6. What are some real-world examples?
Cross-platform UI libraries
Payment gateway integrations
Cloud provider SDK abstractions
Database provider factories
Theme engines
Summary
The Abstract Factory Pattern extends the idea of object creation by producing families of related objects rather than individual instances. It helps ensure compatibility between products, reduces coupling, and makes it easy to switch entire implementations—such as moving from one payment provider or UI theme to another—without changing client code.
While it introduces additional abstraction, it becomes invaluable in enterprise applications where consistency and extensibility are essential. Combined with Dependency Injection in ASP.NET Core, the Abstract Factory Pattern provides a clean and scalable way to manage complex object creation scenarios.
Coming Up Next
In Part 2.4, we'll explore the Builder Design Pattern, including:
What is the Builder Pattern?
Why constructors become difficult to maintain
Builder vs Constructor vs Object Initializer
Fluent Builder Pattern
Immutable Objects with Builder
UML Class Diagram
Complete C# Console Application
ASP.NET Core implementation
Real-world examples
Advantages and disadvantages
Best practices
Common mistakes
Interview questions
You'll learn how the Builder Pattern simplifies the creation of complex objects while improving readability, maintainability, and flexibility in modern C# applications.