Skip to content Skip to footer

Creational Design Patterns: Abstract Factory

Building Product Families That Work Together

Ever shipped a UI where dark mode buttons appeared on light mode backgrounds? Or mixed database queries where SQL Server syntax broke PostgreSQL connections? When objects need to work together as a cohesive family, the Abstract Factory pattern ensures compatibility by design.

The Problem: Managing Related Object Groups

You’re building a cross-platform application that needs UI components. Each platform (Windows, macOS, Linux) has its own styling:

public class OrderForm
{
    public void Display()
    {
        var button = new WindowsButton();
        var textbox = new MacTextBox(); // Oops! Mixing platforms
        var checkbox = new LinuxCheckBox(); // Visual chaos!
        
        button.Render();
        textbox.Render();
        checkbox.Render();
    }
}

Problems:

  • Components from different families get mixed accidentally
  • No compile-time guarantee that objects belong to the same family
  • Adding a new platform requires changes throughout the codebase
  • Testing requires mocking each concrete class individually

The Solution: Abstract Factory Pattern

The Abstract Factory provides an interface for creating families of related objects without specifying their concrete classes. It’s like a factory that produces complete product lines.

Definition: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Pseudocode Structure

AbstractFactory (interface)
├── CreateProductA() : AbstractProductA
└── CreateProductB() : AbstractProductB

ConcreteFactory1 : AbstractFactory
├── CreateProductA() : returns ProductA1
└── CreateProductB() : returns ProductB1

ConcreteFactory2 : AbstractFactory
├── CreateProductA() : returns ProductA2
└── CreateProductB() : returns ProductB2

Client → uses AbstractFactory and AbstractProducts

Real-World Example: Multi-Cloud Infrastructure Manager

Step 1: Define Product Interfaces

// Abstract Products
public interface ICloudStorage
{
    Task<string> UploadFileAsync(string filePath);
    Task<byte[]> DownloadFileAsync(string fileId);
    string GetStorageType();
}

public interface ICloudDatabase
{
    Task<bool> ConnectAsync();
    Task ExecuteQueryAsync(string query);
    string GetDatabaseType();
}

public interface ICloudMessaging
{
    Task SendMessageAsync(string queue, string message);
    Task<string> ReceiveMessageAsync(string queue);
    string GetMessagingType();
}

Step 2: Create Concrete Products for Each Family

// AWS Product Family
public class S3Storage : ICloudStorage
{
    public async Task<string> UploadFileAsync(string filePath)
    {
        await Task.Delay(100);
        return $"s3://bucket/{Path.GetFileName(filePath)}";
    }

    public async Task<byte[]> DownloadFileAsync(string fileId)
    {
        await Task.Delay(100);
        return Array.Empty<byte>();
    }

    public string GetStorageType() => "AWS S3";
}

public class RdsDatabase : ICloudDatabase
{
    public async Task<bool> ConnectAsync()
    {
        await Task.Delay(50);
        Console.WriteLine("Connected to AWS RDS");
        return true;
    }

    public async Task ExecuteQueryAsync(string query)
    {
        await Task.Delay(75);
        Console.WriteLine($"RDS executing: {query}");
    }

    public string GetDatabaseType() => "AWS RDS";
}

public class SqsMessaging : ICloudMessaging
{
    public async Task SendMessageAsync(string queue, string message)
    {
        await Task.Delay(30);
        Console.WriteLine($"SQS sent to {queue}: {message}");
    }

    public async Task<string> ReceiveMessageAsync(string queue)
    {
        await Task.Delay(30);
        return "Message from SQS";
    }

    public string GetMessagingType() => "AWS SQS";
}

// Azure Product Family
public class BlobStorage : ICloudStorage
{
    public async Task<string> UploadFileAsync(string filePath)
    {
        await Task.Delay(100);
        return $"https://storage.blob.core.windows.net/{Path.GetFileName(filePath)}";
    }

    public async Task<byte[]> DownloadFileAsync(string fileId)
    {
        await Task.Delay(100);
        return Array.Empty<byte>();
    }

    public string GetStorageType() => "Azure Blob Storage";
}

public class SqlDatabase : ICloudDatabase
{
    public async Task<bool> ConnectAsync()
    {
        await Task.Delay(50);
        Console.WriteLine("Connected to Azure SQL");
        return true;
    }

    public async Task ExecuteQueryAsync(string query)
    {
        await Task.Delay(75);
        Console.WriteLine($"Azure SQL executing: {query}");
    }

    public string GetDatabaseType() => "Azure SQL";
}

public class ServiceBusMessaging : ICloudMessaging
{
    public async Task SendMessageAsync(string queue, string message)
    {
        await Task.Delay(30);
        Console.WriteLine($"Service Bus sent to {queue}: {message}");
    }

    public async Task<string> ReceiveMessageAsync(string queue)
    {
        await Task.Delay(30);
        return "Message from Service Bus";
    }

    public string GetMessagingType() => "Azure Service Bus";
}

Step 3: Define the Abstract Factory

public interface ICloudFactory
{
    ICloudStorage CreateStorage();
    ICloudDatabase CreateDatabase();
    ICloudMessaging CreateMessaging();
}

Step 4: Implement Concrete Factories

public class AwsCloudFactory : ICloudFactory
{
    public ICloudStorage CreateStorage() => new S3Storage();
    public ICloudDatabase CreateDatabase() => new RdsDatabase();
    public ICloudMessaging CreateMessaging() => new SqsMessaging();
}

public class AzureCloudFactory : ICloudFactory
{
    public ICloudStorage CreateStorage() => new BlobStorage();
    public ICloudDatabase CreateDatabase() => new SqlDatabase();
    public ICloudMessaging CreateMessaging() => new ServiceBusMessaging();
}

Step 5: Client Code

public class CloudApplication
{
    private readonly ICloudStorage _storage;
    private readonly ICloudDatabase _database;
    private readonly ICloudMessaging _messaging;

    public CloudApplication(ICloudFactory factory)
    {
        _storage = factory.CreateStorage();
        _database = factory.CreateDatabase();
        _messaging = factory.CreateMessaging();
    }

    public async Task RunWorkflowAsync()
    {
        Console.WriteLine($"Using: {_storage.GetStorageType()}, " +
                         $"{_database.GetDatabaseType()}, " +
                         $"{_messaging.GetMessagingType()}");

        await _database.ConnectAsync();
        var fileId = await _storage.UploadFileAsync("data.json");
        await _messaging.SendMessageAsync("processing-queue", fileId);
        await _database.ExecuteQueryAsync("INSERT INTO uploads...");
    }
}

// Usage
public class Program
{
    public static async Task Main(string[] args)
    {
        // Switch entire cloud provider with one line
        ICloudFactory factory = GetCloudFactory("Azure");
        
        var app = new CloudApplication(factory);
        await app.RunWorkflowAsync();
    }

    private static ICloudFactory GetCloudFactory(string provider)
    {
        return provider switch
        {
            "AWS" => new AwsCloudFactory(),
            "Azure" => new AzureCloudFactory(),
            _ => throw new ArgumentException("Unknown provider")
        };
    }
}

Advanced Example: UI Theme System

public interface IButton
{
    void Render();
    void OnClick(Action callback);
}

public interface ITextBox
{
    void Render();
    string GetValue();
}

public interface IThemeFactory
{
    IButton CreateButton(string text);
    ITextBox CreateTextBox(string placeholder);
}

public class DarkThemeFactory : IThemeFactory
{
    public IButton CreateButton(string text) => new DarkButton(text);
    public ITextBox CreateTextBox(string placeholder) => new DarkTextBox(placeholder);
}

public class LightThemeFactory : IThemeFactory
{
    public IButton CreateButton(string text) => new LightButton(text);
    public ITextBox CreateTextBox(string placeholder) => new LightTextBox(placeholder);
}

// Concrete implementations
public class DarkButton : IButton
{
    private readonly string _text;
    public DarkButton(string text) => _text = text;
    
    public void Render() => Console.WriteLine($"[Dark Button: {_text}]");
    public void OnClick(Action callback) => callback();
}

public class DarkTextBox : ITextBox
{
    private readonly string _placeholder;
    public DarkTextBox(string placeholder) => _placeholder = placeholder;
    
    public void Render() => Console.WriteLine($"[Dark TextBox: {_placeholder}]");
    public string GetValue() => "user input";
}

// Application
public class FormBuilder
{
    private readonly IThemeFactory _factory;

    public FormBuilder(IThemeFactory factory) => _factory = factory;

    public void BuildLoginForm()
    {
        var usernameBox = _factory.CreateTextBox("Enter username");
        var passwordBox = _factory.CreateTextBox("Enter password");
        var submitButton = _factory.CreateButton("Login");

        usernameBox.Render();
        passwordBox.Render();
        submitButton.Render();
    }
}

Enterprise Applications

1. Multi-Tenant SaaS Platforms

Create tenant-specific configurations (database connections, storage providers, email services) that work together seamlessly.

2. Cross-Platform Mobile Apps

Generate UI components, file systems, and notification handlers specific to iOS, Android, or desktop platforms.

3. Trading Systems

Build market data feeds, order execution engines, and risk calculators tailored to different exchanges or instruments.

4. Report Generation

Create formatters, renderers, and exporters that work together for PDF, Excel, or web-based reporting.

5. Gaming Engines

Produce graphics renderers, audio engines, and input handlers optimized for different gaming platforms.

Benefits

  • Consistency Guaranteed: All products from a factory are compatible
  • Isolation: Swapping entire product families requires changing only the factory
  • Single Responsibility: Each factory creates one cohesive family
  • Easy Testing: Mock entire ecosystems with test factories

When to Use

Use Abstract Factory when:

  • Your system needs to work with multiple families of related products
  • You want to enforce constraints that products from one family are used together
  • You want to provide a library of products revealing only interfaces
  • The product family may expand, but relationships remain stable

When NOT to Use

Avoid this pattern when:

  • You only have one product family
  • Products don’t have dependencies on each other
  • The added complexity outweighs the benefits
  • Your product families change structure frequently (not just add new families)

Comparison with Factory Method

  • Factory Method: Creates one product type with multiple variations
  • Abstract Factory: Creates multiple related product types as families

Key Takeaway: Abstract Factory ensures that related objects work together harmoniously by grouping their creation into cohesive factories, preventing incompatible combinations and enabling wholesale ecosystem changes with minimal code modifications.

Leave a Comment