Imagine an application that runs on more than one cloud. On Azure it stores files in Blob Storage and sends messages through Service Bus; on AWS it uses S3 and SQS. Mixing them, Azure storage with AWS messaging, is a bug waiting to happen. And your business code shouldn't know which cloud it's talking to.
The Factory Method creates one object. The Abstract Factory creates a family of objects that belong together.
The classic structure
The Gang of Four put it like this:
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Four roles: the AbstractFactory declares one creation method per product, each ConcreteFactory creates the products of one family, the AbstractProducts are the interfaces, and the ConcreteProducts are designed to work with the other products of the same family.
The textbook example is a UI toolkit:
public interface IButton { void Render(); } public interface ICheckbox { void Render(); } public class WindowsButton : IButton { public void Render() => Console.WriteLine("Rendering Windows-style button"); } public class WindowsCheckbox : ICheckbox { public void Render() => Console.WriteLine("Rendering Windows-style checkbox"); } // The abstract factory: one method per product of the family public interface IUIFactory { IButton CreateButton(); ICheckbox CreateCheckbox(); } // One concrete factory per family public class WindowsUIFactory : IUIFactory { public IButton CreateButton() => new WindowsButton(); public ICheckbox CreateCheckbox() => new WindowsCheckbox(); } A LinuxUIFactory would do the same with Linux controls. The client receives an IUIFactory and can never mix a Windows button with a Linux checkbox.
The price: adding a new product type (say, ISlider) means changing the interface and every concrete factory.
In modern .NET: one family per key
The same idea fits naturally with keyed services and configuration. Here's the cloud example:
// The family: every product a client needs from one cloud provider public interface ICloudServicesFactory { ICloudStorage CreateStorage(); ICloudMessaging CreateMessaging(); } public enum CloudProvider { Azure, Aws } // One concrete factory per family: its products always match each other public sealed class AzureServicesFactory(ILoggerFactory loggers) : ICloudServicesFactory { public ICloudStorage CreateStorage() => new AzureBlobStorage(loggers.CreateLogger<AzureBlobStorage>()); public ICloudMessaging CreateMessaging() => new ServiceBusMessaging(loggers.CreateLogger<ServiceBusMessaging>()); } public sealed class AwsServicesFactory(ILoggerFactory loggers) : ICloudServicesFactory { public ICloudStorage CreateStorage() => new S3Storage(loggers.CreateLogger<S3Storage>()); public ICloudMessaging CreateMessaging() => new SqsMessaging(loggers.CreateLogger<SqsMessaging>()); } Each family is registered under its own key, and the configuration picks one at startup:
builder.Services.AddKeyedSingleton<ICloudServicesFactory, AzureServicesFactory>(CloudProvider.Azure); builder.Services.AddKeyedSingleton<ICloudServicesFactory, AwsServicesFactory>(CloudProvider.Aws); builder.Services.AddSingleton<ICloudServicesFactory>(sp => { var provider = sp.GetRequiredService<IConfiguration>().GetValue<CloudProvider>("Cloud:Provider"); return sp.GetRequiredKeyedService<ICloudServicesFactory>(provider); }); The client sees only the family interface: no keys, no casts, no IServiceProvider.
public sealed class DocumentArchiver(ICloudServicesFactory cloud) { public async Task ArchiveAsync(string name, Stream content, CancellationToken cancellationToken) { var storage = cloud.CreateStorage(); var messaging = cloud.CreateMessaging(); await storage.UploadAsync(name, content, cancellationToken); await messaging.PublishAsync($"archived:{name}", cancellationToken); } } Switching cloud is a configuration change. Adding a third provider is a new factory plus one registration, and the business code doesn't move.
When to use it, and when not
Use it when:
- you create families of objects that must be used together (database providers, cloud providers, UI themes);
- you want to make it impossible to mix incompatible implementations;
- the whole family is chosen at runtime, from configuration or context;
- you're building a framework or library that supports several implementations.
Skip it when:
- you only create single objects: a Factory Method, or the DI container, is enough;
- the families will never change or grow;
- the product types change often: every new product touches every factory.
A few rules
- One interface per family, typed methods per product. Avoid generic
Create<T>(string key)factories: they hide errors until runtime. - Choose the family once, at startup, in the composition root.
- Keep
IServiceProviderout of business code. The container builds the factories; clients only receive them. - Don't pool what the platform already pools: ADO.NET pools connections,
IHttpClientFactorypools handlers.
Want the full story?
This is the short version. The complete article on Level Up Coding follows the Abstract Factory from 1994 to today: the GoF structure and its UML, database provider factories, configuration-driven and generic factories, Dependency Injection, asynchronous factories for the cloud, functional and pattern-matching variants, pooling, testing, distributed and serverless factories, and source generators.
Read it here: Abstract Factory Pattern in .NET C#: From Legacy Systems to Cloud-Native Architecture.
Thanks for reading.


