How can the Factory Pattern be applied to select different business service implementations at runtime?
Question
Answers
The Factory Pattern becomes valuable when an application has a common business operation but multiple implementations.
For example:
IPaymentProcessor
|
+---- CardPaymentProcessor
|
+---- BankTransferProcessor
|
+---- WalletPaymentProcessorThe calling code should ideally not need to know how each implementation is constructed.
Instead:
Business Service
|
v
Payment Factory
|
+---- Card
+---- Bank
+---- WalletThe 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 CFor example:
Invoice Processing
|
+--> DomesticInvoiceStrategy
|
+--> InternationalInvoiceStrategy
|
+--> GovernmentInvoiceStrategyThe 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 Strategyrather than:
Every class
|
v
IServiceProvider
|
+--> Anything
+--> Anything else
+--> Something elseRuntime 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 APIThat 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 AdapterThe application layer works against:
IIntegrationHandlerrather 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