← Back to Article List         
Abstract Factory Pattern

Abstract Factory Pattern

Published on 27 Sep 2026     14 min read Design Principles & Patterns
Abstract Pattern

Abstract Factory Pattern in ASP.NET Core Web API with DI

We'll use the same Notification example, but implement it the way you would normally structure it in an ASP.NET Core Web API using Dependency Injection.

The example supports two notification families:

Email
 ├── EmailNotification
 └── EmailLogger

SMS
 ├── SmsNotification
 └── SmsLogger

ASP.NET Core DI will create and inject the required factories and services.


1. What is Abstract Factory Pattern?

Abstract Factory is a Creational Design Pattern used to create a family of related objects without the client directly creating their concrete classes.

Simple definition:

Abstract Factory creates a group of related objects through a common factory interface.

In our Web API:

Email Factory
    ├── Email Notification
    └── Email Logger

SMS Factory
    ├── SMS Notification
    └── SMS Logger

The controller doesn't need to write:

new EmailNotification();
new EmailLogger();

Instead, it works through abstractions and injected factories.


2. Why Do We Need It in Web API?

Without Abstract Factory, a controller might look like this:

[HttpPost]
public IActionResult Send(string type, string message)
{
    if (type == "email")
    {
        var notification = new EmailNotification();
        var logger = new EmailLogger();

        notification.Send(message);
        logger.Log("Email sent");
    }
    else if (type == "sms")
    {
        var notification = new SmsNotification();
        var logger = new SmsLogger();

        notification.Send(message);
        logger.Log("SMS sent");
    }

    return Ok();
}

It works, but the controller knows about all concrete implementations:

Controller
   │
   ├── EmailNotification
   ├── EmailLogger
   ├── SmsNotification
   └── SmsLogger

As the application grows, you might add:

WhatsAppNotification
WhatsAppLogger

PushNotification
PushLogger

The controller becomes responsible for too much object-creation logic.

With Abstract Factory:

Controller
    ↓
Factory Provider / Resolver
    ↓
INotificationFactory
    ↓
Concrete Factory
    ↓
Related Products

The controller deals mainly with abstractions.


3. What Does DI Do Here?

There are two different concepts working together.

Abstract Factory

Decides which family of related objects should be used.

Dependency Injection

Creates registered objects and supplies their dependencies automatically.

For example:

public EmailNotificationFactory(
    EmailNotification notification,
    EmailLogger logger)
{
    _notification = notification;
    _logger = logger;
}

You do not manually pass these objects.

ASP.NET Core DI sees the constructor and resolves them from the service container.

Conceptually:

ASP.NET Core DI Container
        ↓
EmailNotificationFactory
        ↓
needs EmailNotification + EmailLogger
        ↓
DI creates/resolves both
        ↓
injects them into constructor

4. Project Structure

A simple Web API structure:

AbstractFactoryWebApi
│
├── Controllers
│   └── NotificationController.cs
│
├── Interfaces
│   ├── INotification.cs
│   ├── INotificationLogger.cs
│   └── INotificationFactory.cs
│
├── Notifications
│   ├── EmailNotification.cs
│   └── SmsNotification.cs
│
├── Loggers
│   ├── EmailLogger.cs
│   └── SmsLogger.cs
│
├── Factories
│   ├── EmailNotificationFactory.cs
│   ├── SmsNotificationFactory.cs
│   └── NotificationFactoryProvider.cs
│
├── Models
│   └── NotificationRequest.cs
│
└── Program.cs

5. Step 1 — Create Notification Interface

public interface INotification
{
    void Send(string message);
}

This represents our first product type.

Concrete implementations will be:

EmailNotification
SmsNotification

6. Step 2 — Create Logger Interface

I'll name it INotificationLogger rather than ILogger because ASP.NET Core already has the commonly used ILogger<T> abstraction.

public interface INotificationLogger
{
    void Log(string message);
}

This is our second product type.

Concrete implementations:

EmailLogger
SmsLogger

Therefore, our Abstract Factory creates two related products:

INotification
INotificationLogger

7. Step 3 — Email Notification

public class EmailNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

8. Step 4 — Email Logger

public class EmailLogger : INotificationLogger
{
    public void Log(string message)
    {
        Console.WriteLine($"Email Log: {message}");
    }
}

These belong to the same Email family:

Email Family
    ├── EmailNotification
    └── EmailLogger

9. Step 5 — SMS Notification

public class SmsNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine($"SMS sent: {message}");
    }
}

10. Step 6 — SMS Logger

public class SmsLogger : INotificationLogger
{
    public void Log(string message)
    {
        Console.WriteLine($"SMS Log: {message}");
    }
}

These belong to:

SMS Family
    ├── SmsNotification
    └── SmsLogger

11. Step 7 — Create Abstract Factory

Now comes the main part.

public interface INotificationFactory
{
    INotification CreateNotification();

    INotificationLogger CreateLogger();
}

This is our Abstract Factory.

It says:

Every notification factory must provide a notification and a matching logger.

But it doesn't know whether those objects are:

EmailNotification + EmailLogger

or:

SmsNotification + SmsLogger

The concrete factory decides that.


12. Step 8 — Email Factory with DI

public class EmailNotificationFactory : INotificationFactory
{
    private readonly EmailNotification _notification;
    private readonly EmailLogger _logger;

    public EmailNotificationFactory(
        EmailNotification notification,
        EmailLogger logger)
    {
        _notification = notification;
        _logger = logger;
    }

    public INotification CreateNotification()
    {
        return _notification;
    }

    public INotificationLogger CreateLogger()
    {
        return _logger;
    }
}

Notice something important.

We are not doing this:

return new EmailNotification();

Instead, the objects are constructor-injected:

public EmailNotificationFactory(
    EmailNotification notification,
    EmailLogger logger)

ASP.NET Core DI provides them.

So:

DI Container
     ↓
creates EmailNotification
     ↓
creates EmailLogger
     ↓
creates EmailNotificationFactory
     ↓
injects both

13. Step 9 — SMS Factory with DI

public class SmsNotificationFactory : INotificationFactory
{
    private readonly SmsNotification _notification;
    private readonly SmsLogger _logger;

    public SmsNotificationFactory(
        SmsNotification notification,
        SmsLogger logger)
    {
        _notification = notification;
        _logger = logger;
    }

    public INotification CreateNotification()
    {
        return _notification;
    }

    public INotificationLogger CreateLogger()
    {
        return _logger;
    }
}

Again, no manual new.

ASP.NET Core creates and injects:

SmsNotification
SmsLogger

14. We Now Have Two Concrete Factories

Our design currently looks like:

                INotificationFactory
                        ▲
                        │
          ┌─────────────┴─────────────┐
          │                           │
EmailNotificationFactory     SmsNotificationFactory
          │                           │
    ┌─────┴─────┐               ┌─────┴─────┐
    │           │               │           │
    ▼           ▼               ▼           ▼
 Email        Email            SMS         SMS
Notification Logger        Notification   Logger

But now we have another question.

How does the controller choose Email or SMS?

We need a small factory provider/resolver.


15. Step 10 — Notification Factory Provider

public class NotificationFactoryProvider
{
    private readonly EmailNotificationFactory _emailFactory;
    private readonly SmsNotificationFactory _smsFactory;

    public NotificationFactoryProvider(
        EmailNotificationFactory emailFactory,
        SmsNotificationFactory smsFactory)
    {
        _emailFactory = emailFactory;
        _smsFactory = smsFactory;
    }

    public INotificationFactory GetFactory(string type)
    {
        return type.ToLower() switch
        {
            "email" => _emailFactory,
            "sms" => _smsFactory,

            _ => throw new ArgumentException(
                "Invalid notification type")
        };
    }
}

ASP.NET Core DI will also inject:

EmailNotificationFactory
SmsNotificationFactory

into this provider.

The provider only decides:

"email"
    ↓
EmailNotificationFactory

"sms"
    ↓
SmsNotificationFactory

16. Step 11 — Request Model

public class NotificationRequest
{
    public string Type { get; set; } = string.Empty;

    public string Message { get; set; } = string.Empty;
}

Example API request:

{
  "type": "email",
  "message": "Your order has been confirmed."
}

17. Step 12 — Controller

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
    private readonly NotificationFactoryProvider _factoryProvider;

    public NotificationController(
        NotificationFactoryProvider factoryProvider)
    {
        _factoryProvider = factoryProvider;
    }

    [HttpPost]
    public IActionResult Send(NotificationRequest request)
    {
        var factory =
            _factoryProvider.GetFactory(request.Type);

        var notification =
            factory.CreateNotification();

        var logger =
            factory.CreateLogger();

        notification.Send(request.Message);

        logger.Log(
            $"{request.Type} notification sent successfully.");

        return Ok(new
        {
            Message = "Notification sent successfully"
        });
    }
}

Notice that the controller doesn't create:

new EmailNotification();
new EmailLogger();
new SmsNotification();
new SmsLogger();

It asks the provider for the appropriate factory.


18. Step 13 — Register Everything in DI

Now we need to tell ASP.NET Core about our classes.

In Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();


// Products
builder.Services.AddScoped<EmailNotification>();
builder.Services.AddScoped<EmailLogger>();

builder.Services.AddScoped<SmsNotification>();
builder.Services.AddScoped<SmsLogger>();


// Concrete Factories
builder.Services.AddScoped<EmailNotificationFactory>();
builder.Services.AddScoped<SmsNotificationFactory>();


// Factory Provider
builder.Services.AddScoped<NotificationFactoryProvider>();


var app = builder.Build();

app.MapControllers();

app.Run();

This step is mandatory for constructor injection of these types.


19. Full Program

INotification.cs

public interface INotification
{
    void Send(string message);
}

INotificationLogger.cs

public interface INotificationLogger
{
    void Log(string message);
}

INotificationFactory.cs

public interface INotificationFactory
{
    INotification CreateNotification();

    INotificationLogger CreateLogger();
}

EmailNotification.cs

public class EmailNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

EmailLogger.cs

public class EmailLogger : INotificationLogger
{
    public void Log(string message)
    {
        Console.WriteLine($"Email Log: {message}");
    }
}

SmsNotification.cs

public class SmsNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine($"SMS sent: {message}");
    }
}

SmsLogger.cs

public class SmsLogger : INotificationLogger
{
    public void Log(string message)
    {
        Console.WriteLine($"SMS Log: {message}");
    }
}

EmailNotificationFactory.cs

public class EmailNotificationFactory : INotificationFactory
{
    private readonly EmailNotification _notification;
    private readonly EmailLogger _logger;

    public EmailNotificationFactory(
        EmailNotification notification,
        EmailLogger logger)
    {
        _notification = notification;
        _logger = logger;
    }

    public INotification CreateNotification()
    {
        return _notification;
    }

    public INotificationLogger CreateLogger()
    {
        return _logger;
    }
}

SmsNotificationFactory.cs

public class SmsNotificationFactory : INotificationFactory
{
    private readonly SmsNotification _notification;
    private readonly SmsLogger _logger;

    public SmsNotificationFactory(
        SmsNotification notification,
        SmsLogger logger)
    {
        _notification = notification;
        _logger = logger;
    }

    public INotification CreateNotification()
    {
        return _notification;
    }

    public INotificationLogger CreateLogger()
    {
        return _logger;
    }
}

NotificationFactoryProvider.cs

public class NotificationFactoryProvider
{
    private readonly EmailNotificationFactory _emailFactory;
    private readonly SmsNotificationFactory _smsFactory;

    public NotificationFactoryProvider(
        EmailNotificationFactory emailFactory,
        SmsNotificationFactory smsFactory)
    {
        _emailFactory = emailFactory;
        _smsFactory = smsFactory;
    }

    public INotificationFactory GetFactory(string type)
    {
        return type.ToLower() switch
        {
            "email" => _emailFactory,
            "sms" => _smsFactory,

            _ => throw new ArgumentException(
                "Invalid notification type")
        };
    }
}

NotificationRequest.cs

public class NotificationRequest
{
    public string Type { get; set; } = string.Empty;

    public string Message { get; set; } = string.Empty;
}

NotificationController.cs

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
    private readonly NotificationFactoryProvider _factoryProvider;

    public NotificationController(
        NotificationFactoryProvider factoryProvider)
    {
        _factoryProvider = factoryProvider;
    }

    [HttpPost]
    public IActionResult Send(NotificationRequest request)
    {
        var factory =
            _factoryProvider.GetFactory(request.Type);

        var notification =
            factory.CreateNotification();

        var logger =
            factory.CreateLogger();

        notification.Send(request.Message);

        logger.Log(
            $"{request.Type} notification sent successfully.");

        return Ok(new
        {
            Message = "Notification sent successfully"
        });
    }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();


// Products
builder.Services.AddScoped<EmailNotification>();
builder.Services.AddScoped<EmailLogger>();

builder.Services.AddScoped<SmsNotification>();
builder.Services.AddScoped<SmsLogger>();


// Factories
builder.Services.AddScoped<EmailNotificationFactory>();
builder.Services.AddScoped<SmsNotificationFactory>();


// Factory Provider
builder.Services.AddScoped<NotificationFactoryProvider>();


var app = builder.Build();

app.MapControllers();

app.Run();

20. Test the API

Call:

POST /api/notification
Content-Type: application/json

Request:

{
  "type": "email",
  "message": "Your order has been confirmed."
}

Console:

Email sent: Your order has been confirmed.

Email Log: email notification sent successfully.

API response:

{
  "message": "Notification sent successfully"
}

21. What Exactly Happens When API Is Called?

This is the most important part to understand.

When the HTTP request arrives:

POST /api/notification
        ↓
NotificationController

ASP.NET Core needs to create:

NotificationController

It examines its constructor:

public NotificationController(
    NotificationFactoryProvider factoryProvider)

ASP.NET Core asks:

Do I know how to create NotificationFactoryProvider?

Yes, because we registered:

builder.Services.AddScoped<NotificationFactoryProvider>();

But that class itself has dependencies:

public NotificationFactoryProvider(
    EmailNotificationFactory emailFactory,
    SmsNotificationFactory smsFactory)

So DI resolves those.


22. DI Dependency Chain

For EmailNotificationFactory, DI sees:

public EmailNotificationFactory(
    EmailNotification notification,
    EmailLogger logger)

Therefore the complete chain is:

HTTP Request
     ↓
NotificationController
     │
     │ requires
     ↓
NotificationFactoryProvider
     │
     ├───────────────┐
     ↓               ↓
EmailFactory      SmsFactory
     │               │
 ┌───┴───┐       ┌───┴───┐
 ↓       ↓       ↓       ↓
Email   Email    SMS     SMS
Notif.  Logger   Notif.  Logger

ASP.NET Core DI constructs this object graph automatically.

You don't manually pass constructor parameters.


23. What Happens for an Email Request?

Request:

{
  "type": "email",
  "message": "Payment successful"
}

Controller executes:

var factory =
    _factoryProvider.GetFactory(request.Type);

Because:

request.Type = "email"

the provider returns:

EmailNotificationFactory

Then:

var notification =
    factory.CreateNotification();

returns:

EmailNotification

And:

var logger =
    factory.CreateLogger();

returns:

EmailLogger

Final execution:

Request
  │
  │ type = email
  ↓
NotificationController
  ↓
NotificationFactoryProvider
  ↓
EmailNotificationFactory
  │
  ├── CreateNotification()
  │          ↓
  │    EmailNotification
  │
  └── CreateLogger()
             ↓
        EmailLogger

24. What Happens for SMS?

Request:

{
  "type": "sms",
  "message": "OTP is 123456"
}

Flow:

Request
  │
  │ type = sms
  ↓
NotificationController
  ↓
NotificationFactoryProvider
  ↓
SmsNotificationFactory
  │
  ├── SmsNotification
  │
  └── SmsLogger

The controller code itself does not change.

That's one of the major benefits.


25. Why Are We Injecting Concrete Classes?

You may notice this:

public EmailNotificationFactory(
    EmailNotification notification,
    EmailLogger logger)

instead of:

public EmailNotificationFactory(
    INotification notification,
    INotificationLogger logger)

There is a reason.

If we registered:

builder.Services.AddScoped<INotification, EmailNotification>();
builder.Services.AddScoped<INotification, SmsNotification>();

then DI has multiple implementations of the same interface.

For this simple example, explicitly injecting:

EmailNotification
EmailLogger

makes the relationship very clear:

EmailFactory
    ↓
Email products

and:

SmsFactory
    ↓
SMS products

There are more advanced approaches using keyed services in modern .NET, but I would first understand the Abstract Factory pattern with this simpler implementation.


26. Why AddScoped?

We registered:

builder.Services.AddScoped<EmailNotification>();
builder.Services.AddScoped<EmailLogger>();

builder.Services.AddScoped<SmsNotification>();
builder.Services.AddScoped<SmsLogger>();

Scoped means:

One object instance is created per HTTP request/service scope.

For example:

HTTP Request 1
    ↓
EmailNotification Instance A

HTTP Request 2
    ↓
EmailNotification Instance B

For many Web API services, Scoped is a common choice, especially when request-scoped dependencies such as EF Core DbContext may be involved.

It is not a rule that every factory must be scoped; the appropriate lifetime depends on its dependencies and state.


27. Abstract Factory + DI Responsibilities

Don't confuse these two concepts.

Abstract Factory Dependency Injection
Design pattern Dependency management mechanism
Creates/selects related product families Resolves registered dependencies
Defines factory abstraction Maintains service registrations
Controls product-family creation Creates the object graph
Application design Framework infrastructure

In our example:

ASP.NET Core DI
      ↓
Creates factories and their dependencies

Abstract Factory
      ↓
Represents the related product family

Factory Provider
      ↓
Selects Email or SMS family

28. Factory Pattern vs Abstract Factory in Web API

This is important because your previous example was a Factory Pattern.

Factory Pattern

Your earlier design was roughly:

public INotification CreateNotification(
    NotificationRequest request)
{
    return request.Type.ToLower() switch
    {
        "email" => new EmailNotification(...),
        "sms" => new SmsNotification(...),
        _ => throw new Exception()
    };
}

It creates one product hierarchy:

INotification
      │
 ┌────┴─────┐
 ↓          ↓
Email      SMS

Abstract Factory

Now we have multiple related products:

                INotificationFactory
                         │
          ┌──────────────┴──────────────┐
          ↓                             ↓
     EmailFactory                   SmsFactory
          │                             │
     ┌────┴────┐                   ┌────┴────┐
     ↓         ↓                   ↓         ↓
   Email     Email                SMS       SMS
   Notif.    Logger              Notif.    Logger

That's the fundamental difference.

Factory

Factory
  ↓
One Product

Abstract Factory

Abstract Factory
       ↓
Family of Related Products

29. Advantages in Web API

1. Loose Coupling

The controller doesn't create concrete notification objects:

new EmailNotification();
new SmsNotification();

Object creation is separated from API logic.

2. Works Well with ASP.NET Core DI

Constructor injection handles dependencies automatically.

public EmailNotificationFactory(
    EmailNotification notification,
    EmailLogger logger)

No manual parameter passing is required.

3. Keeps Related Objects Together

Email:

EmailNotification
EmailLogger

SMS:

SmsNotification
SmsLogger

This prevents product families from being scattered throughout the application.

4. Cleaner Controllers

The controller concentrates on HTTP/application flow rather than object construction.

var factory =
    _factoryProvider.GetFactory(request.Type);

5. Easier Testing

Because responsibilities are separated, factories and controllers can be tested independently.

6. Easier to Add Product Families

For example:

WhatsAppNotification
WhatsAppLogger
WhatsAppNotificationFactory

The existing Email and SMS factories don't need to be rewritten.


30. Disadvantages

Abstract Factory is not something you should add to every Web API.

It introduces more:

Interfaces
Classes
Factories
DI registrations
Abstractions

For example, if your application has only:

EmailNotification

then creating:

INotificationFactory
EmailNotificationFactory
NotificationFactoryProvider

would probably be unnecessary.

Use Abstract Factory when you genuinely have multiple related product families.


31. Real Production-Type Example

A stronger real-world Web API example would be payment providers.

Suppose your API supports:

Stripe
PayPal

Each provider needs:

Payment Processor
Refund Processor
Payment Validator

Then:

IPaymentFactory
      │
      ├── CreatePaymentProcessor()
      ├── CreateRefundProcessor()
      └── CreateValidator()

Stripe:

StripePaymentFactory
      │
      ├── StripePaymentProcessor
      ├── StripeRefundProcessor
      └── StripeValidator

PayPal:

PayPalPaymentFactory
      │
      ├── PayPalPaymentProcessor
      ├── PayPalRefundProcessor
      └── PayPalValidator

The controller can work with:

IPaymentFactory

instead of containing Stripe- and PayPal-specific object creation everywhere.

That is a much stronger use case for Abstract Factory.


32. Important Interview Question

Who creates EmailNotificationFactory?

ASP.NET Core DI container.

Because we registered:

builder.Services.AddScoped<EmailNotificationFactory>();

Who passes EmailNotification and EmailLogger to its constructor?

ASP.NET Core DI container.

Because they are registered:

builder.Services.AddScoped<EmailNotification>();
builder.Services.AddScoped<EmailLogger>();

So when DI encounters:

public EmailNotificationFactory(
    EmailNotification notification,
    EmailLogger logger)

you don't write:

new EmailNotificationFactory(
    new EmailNotification(),
    new EmailLogger());

ASP.NET Core resolves the dependency graph automatically.


33. Complete Flow

Client
  │
  │ POST /api/notification
  │ { type: "email" }
  ↓
NotificationController
  │
  │ injected by DI
  ↓
NotificationFactoryProvider
  │
  │ GetFactory("email")
  ↓
EmailNotificationFactory
  │
  ├──────────────┐
  ↓              ↓
EmailNotification   EmailLogger
  │              │
  ↓              ↓
Send()           Log()

For SMS:

Client
  ↓
NotificationController
  ↓
NotificationFactoryProvider
  ↓
SmsNotificationFactory
  │
  ├──────────────┐
  ↓              ↓
SmsNotification     SmsLogger
  ↓              ↓
Send()           Log()

Key Points

  • Abstract Factory is a Creational Design Pattern.
  • It is used to create families of related objects.
  • In this example, the families are Email and SMS.
  • Each family contains a Notification + Logger.
  • INotification and INotificationLogger are Abstract Products.
  • EmailNotification, SmsNotification, etc. are Concrete Products.
  • INotificationFactory is the Abstract Factory.
  • EmailNotificationFactory and SmsNotificationFactory are Concrete Factories.
  • NotificationFactoryProvider selects the appropriate factory based on the request.
  • ASP.NET Core DI creates the factories and injects their dependencies automatically.
  • You do not manually pass constructor dependencies when ASP.NET Core resolves a registered service.
  • AddScoped() creates/resolves services within the HTTP request scope.
  • DI and Abstract Factory are different concepts: DI constructs the object graph; Abstract Factory organizes creation/access to related product families.
  • Factory Pattern generally focuses on one product hierarchy.
  • Abstract Factory focuses on a family containing multiple related product types.
  • Use Abstract Factory when your Web API needs interchangeable families of related services/objects.
  • Don't use it for a simple single-service scenario where it only adds unnecessary abstraction.

Interview definition

Abstract Factory is a creational design pattern that provides an interface for creating families of related objects without requiring the client to directly depend on their concrete classes. In ASP.NET Core Web API, Dependency Injection can construct and inject the concrete factories and their dependencies, while the Abstract Factory organizes the related product families.