Skip to content
Community Question

How can the Factory Pattern be applied to select different business service implementations at runtime?

Share knowledge. Learn from experts. Build together.

Question

What if an application needs to choose different business logic depending on the customer, transaction type, or configuration? The Factory Pattern provides a clean approach for creating the appropriate service implementation at runtime. Explore how this improves maintainability, reduces conditional logic, and supports extensible .NET enterprise applications.
63 Views Community Discussion

Answers

The Factory Pattern becomes valuable when an application has a common business operation but multiple implementations.

For example:

IPaymentProcessor
       |
       +---- CardPaymentProcessor
       |
       +---- BankTransferProcessor
       |
       +---- WalletPaymentProcessor

The calling code should ideally not need to know how each implementation is constructed.

Instead:

Business Service
      |
      v
Payment Factory
      |
      +---- Card
      +---- Bank
      +---- Wallet

The factory decides which implementation should be used.

Why use a factory?

Without a factory, developers often end up with:

if (paymentType == "CARD")
{
    ...
}
else if (paymentType == "BANK")
{
    ...
}
else if (paymentType == "WALLET")
{
    ...
}

As the system grows:

CARD
BANK
WALLET
UPI
PAYPAL
INVOICE
...

the business service becomes increasingly coupled to implementation selection.

A factory moves that responsibility into a dedicated component.

Basic design

public interface IPaymentProcessor
{
    Task ProcessAsync(Payment payment);
}

Implementations:

public class CardPaymentProcessor : IPaymentProcessor
{
    public Task ProcessAsync(Payment payment)
    {
        // Card processing
        return Task.CompletedTask;
    }
}

public class BankPaymentProcessor : IPaymentProcessor
{
    public Task ProcessAsync(Payment payment)
    {
        // Bank processing
        return Task.CompletedTask;
    }
}

Factory:

public interface IPaymentProcessorFactory
{
    IPaymentProcessor Create(string paymentType);
}

Implementation:

public class PaymentProcessorFactory : IPaymentProcessorFactory
{
    private readonly IServiceProvider _serviceProvider;

    public PaymentProcessorFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IPaymentProcessor Create(string paymentType)
    {
        return paymentType.ToUpperInvariant() switch
        {
            "CARD" =>
                _serviceProvider.GetRequiredService<CardPaymentProcessor>(),

            "BANK" =>
                _serviceProvider.GetRequiredService<BankPaymentProcessor>(),

            _ =>
                throw new NotSupportedException(
                    $"Payment type '{paymentType}' is not supported.")
        };
    }
}

This is a straightforward factory, but enterprise applications need to consider whether the factory should use explicit mappings, strategy registration, keyed DI, or a more sophisticated resolver.

Factory + Strategy Pattern

The two patterns frequently work together.

The Strategy Pattern encapsulates different algorithms.

The Factory Pattern decides which strategy should be used.

                  Request
                     |
                     v
              Strategy Factory
                     |
        +------------+------------+
        |            |            |
        v            v            v
     Strategy A   Strategy B   Strategy C

For example:

Invoice Processing
       |
       +--> DomesticInvoiceStrategy
       |
       +--> InternationalInvoiceStrategy
       |
       +--> GovernmentInvoiceStrategy

The strategy contains the behavior.

The factory determines which strategy is appropriate.

Modern .NET approach: keyed services

Modern .NET dependency injection supports keyed services, allowing multiple implementations to be registered with different keys. Microsoft documents keyed DI as a way to register and retrieve different service implementations based on a key.

For example:

services.AddKeyedScoped<INotificationService,
    EmailNotificationService>("email");

services.AddKeyedScoped<INotificationService,
    SmsNotificationService>("sms");

A runtime resolver can then retrieve the appropriate service based on a key.

This can reduce the amount of manual construction code in a factory.

But don't turn IServiceProvider into a Service Locator

This is an important architecture distinction.

A factory that receives IServiceProvider can be useful, but if every class starts doing this:

_serviceProvider.GetService<Something>();
_serviceProvider.GetService<SomethingElse>();
_serviceProvider.GetService<AnotherThing>();

the application can effectively become a Service Locator.

That makes dependencies less visible and can make testing harder.

Microsoft's DI guidance recommends avoiding direct instantiation of dependent classes because it couples the service to particular implementations.

A better design is often:

Controller
    |
    v
Application Service
    |
    v
Factory
    |
    v
Specific Strategy

rather than:

Every class
    |
    v
IServiceProvider
    |
    +--> Anything
    +--> Anything else
    +--> Something else

Runtime selection criteria

The factory can select an implementation based on:

  • payment type
  • country
  • tenant
  • customer segment
  • API version
  • business unit
  • document type
  • integration provider
  • feature configuration
  • processing mode

For example:

var processor =
    factory.Create(request.PaymentMethod);

await processor.ProcessAsync(request);

The calling service doesn't need to know whether the implementation is:

Stripe
Azure Service Bus
Dynamics 365
Dataverse
SAP
Internal API

That separation is valuable in enterprise applications.

Avoid giant switch statements

A small switch is not automatically bad.

If there are only two or three stable options:

return type switch
{
    "A" => serviceA,
    "B" => serviceB,
    _ => throw ...
};

may be perfectly reasonable.

The problem occurs when the selection logic becomes a business subsystem of its own.

At that point consider:

  • keyed DI
  • dictionary-based registration
  • strategy registration
  • factory classes
  • metadata-driven selection
  • configuration-driven resolution

Enterprise example

Imagine a Dynamics 365 integration platform:

Business Document
       |
       v
Integration Factory
       |
       +---- Customer
       |       |
       |       +--> Dataverse Adapter
       |
       +---- Invoice
       |       |
       |       +--> ERP Adapter
       |
       +---- Shipment
               |
               +--> Logistics Adapter

The application layer works against:

IIntegrationHandler

rather than directly depending on every external system.

This makes it easier to:

  • add new integrations
  • unit test business logic
  • replace implementations
  • isolate vendor-specific code
  • support multiple tenants
  • evolve architecture

Key points

A good Factory implementation should answer one question:

Which implementation should handle this request?

It should not become responsible for the entire business process.

Keep these responsibilities separate:

Factory       → selects implementation
Strategy      → performs specialized behavior
Service       → coordinates business operation
Repository    → data access
Adapter       → external system integration

Your Answer

Connect