Breaking Free from Hardcoded Creation
If you've ever scattered new operators throughout your code only to realize later that you need to swap implementations or add initialization logic, you've felt the pain of tightly coupled object creation. Changing how objects are built means hunting through dozens of files and updating each construction site individually.
Factory methods centralize object creation behind a method that returns an interface or base class. Callers don't know or care about the concrete type. This lets you change implementations, add validation, or introduce caching without touching code that uses the objects.
You'll build a payment processor factory that selects the right provider based on configuration and amount. By the end, you'll know when factories add value and when they're just extra ceremony.
Creating a Simple Factory Method
A factory method is just a function that returns an instance of a type. Instead of calling new directly, you call the factory. The factory decides which concrete class to instantiate based on parameters or configuration.
Start with a payment processor that has multiple implementations. The factory picks the right one based on the payment amount.
namespace FactoryDemo;
public interface IPaymentProcessor
{
PaymentResult Process(decimal amount, string currency);
}
public class StripeProcessor : IPaymentProcessor
{
public PaymentResult Process(decimal amount, string currency)
{
Console.WriteLine($"Processing ${amount} via Stripe");
return new PaymentResult { Success = true, TransactionId = "stripe_123" };
}
}
public class PayPalProcessor : IPaymentProcessor
{
public PaymentResult Process(decimal amount, string currency)
{
Console.WriteLine($"Processing ${amount} via PayPal");
return new PaymentResult { Success = true, TransactionId = "pp_456" };
}
}
public record PaymentResult
{
public bool Success { get; init; }
public string TransactionId { get; init; } = string.Empty;
}
These processors share an interface but have different implementations. Without a factory, every place that needs a processor would have to decide which one to instantiate. That scatters business logic across your codebase.
Building the Factory
The factory encapsulates the decision logic. It takes the parameters needed to choose an implementation and returns the interface. Callers work with the abstraction and never see concrete types.
namespace FactoryDemo;
public class PaymentProcessorFactory
{
private readonly decimal _stripeThreshold;
public PaymentProcessorFactory(decimal stripeThreshold = 100m)
{
_stripeThreshold = stripeThreshold;
}
public IPaymentProcessor Create(decimal amount)
{
if (amount >= _stripeThreshold)
{
return new StripeProcessor();
}
return new PayPalProcessor();
}
}
The factory hides the selection logic. Change the threshold or add a new processor type, and no calling code needs updates. They still call Create and get back an IPaymentProcessor that works.
Using the Factory
Code that needs a payment processor calls the factory instead of using new. This keeps the creation logic in one place and makes testing easier since you can inject a fake factory.
namespace FactoryDemo;
public class CheckoutService
{
private readonly PaymentProcessorFactory _factory;
public CheckoutService(PaymentProcessorFactory factory)
{
_factory = factory;
}
public PaymentResult ProcessOrder(decimal orderTotal, string currency)
{
var processor = _factory.Create(orderTotal);
return processor.Process(orderTotal, currency);
}
}
CheckoutService doesn't know whether it's getting Stripe or PayPal. It asks the factory for a processor and uses it. If you add a third provider tomorrow, this code stays unchanged.
Factories with Dependencies
Real factories often need to inject dependencies into the objects they create. You can pass constructor parameters through the factory or resolve them from a service container.
namespace FactoryDemo;
public class EnhancedPaymentFactory
{
private readonly ILogger _logger;
private readonly IConfiguration _config;
public EnhancedPaymentFactory(
ILogger logger,
IConfiguration config)
{
_logger = logger;
_config = config;
}
public IPaymentProcessor Create(string providerName)
{
_logger.LogInformation("Creating payment processor: {Provider}", providerName);
return providerName.ToLowerInvariant() switch
{
"stripe" => new StripeProcessor(),
"paypal" => new PayPalProcessor(),
_ => throw new ArgumentException($"Unknown provider: {providerName}")
};
}
}
The factory itself gets dependencies injected and uses them during creation. This lets you combine configuration, logging, and runtime parameters to build the right object with the right setup.
Mistakes to Avoid
Creating god factories: When one factory builds ten unrelated types, it violates single responsibility. The symptom is a factory with a massive switch statement or type parameter. Split it into focused factories, each responsible for creating related objects from a single family.
Returning concrete types: If your factory returns new StripeProcessor() instead of IPaymentProcessor, you've lost the abstraction benefit. Callers can still couple to concrete types. Always return the interface or base class to preserve flexibility and testability.
Factory logic leaking out: When you find the same creation logic duplicated in multiple factories or scattered in calling code, you haven't centralized enough. Consolidate all construction logic in one place so changes only happen once.
Try It Yourself
Build a simple logger factory that creates different loggers based on environment. This shows how factories select implementations at runtime based on external factors.
Steps
- Init the project:
dotnet new console -n FactoryLab
- Move into it:
cd FactoryLab
- Edit Program.cs with the sample below
- Update the .csproj file
- Execute:
dotnet run
var factory = new LoggerFactory("production");
var logger = factory.CreateLogger();
logger.Log("Application started");
interface ILogger
{
void Log(string message);
}
class ConsoleLogger : ILogger
{
public void Log(string message) =>
Console.WriteLine($"[CONSOLE] {message}");
}
class FileLogger : ILogger
{
public void Log(string message) =>
Console.WriteLine($"[FILE] Would write: {message}");
}
class LoggerFactory
{
private readonly string _environment;
public LoggerFactory(string environment) =>
_environment = environment;
public ILogger CreateLogger() =>
_environment == "production"
? new FileLogger()
: new ConsoleLogger();
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Run result
[FILE] Would write: Application started
Knowing the Limits
Skip factories when object creation is trivial and won't change. If you're building a simple DTO or value object with no dependencies and no variation, new is clearer. A factory adds indirection without benefit when there's only ever one concrete type and construction is straightforward.
Avoid factories when dependency injection handles everything. Modern DI containers already know how to construct objects with complex dependency graphs. Adding a factory on top just creates extra layers. Use DI directly unless you need runtime parameters that the container can't provide.
Watch for factories that become change magnets. If every new feature requires updating the factory, you've centralized too much logic there. The factory should make object creation decisions, not business decisions. Move business logic into strategies or services where it belongs.