Factory Pattern Real-World Examples
Production-ready examples demonstrating the Factory pattern in real-world scenarios.
Example 1: Payment Gateway Integration
The Problem
An e-commerce system needs to support multiple payment gateways (Stripe, PayPal, Square) with the ability to add new gateways without modifying existing code.
The Solution
Use Factory to register payment processors by gateway name.
The Code
public interface IPaymentGateway
{
Task<PaymentResult> ProcessAsync(PaymentRequest request, CancellationToken ct);
Task<RefundResult> RefundAsync(string transactionId, decimal amount, CancellationToken ct);
}
public class PaymentGatewayFactory
{
private readonly Factory<string, IServiceProvider, IPaymentGateway> _factory;
public PaymentGatewayFactory(IConfiguration config)
{
_factory = Factory<string, IServiceProvider, IPaymentGateway>
.Create(StringComparer.OrdinalIgnoreCase)
.Map("stripe", (in IServiceProvider sp) => new StripeGateway(
sp.GetRequiredService<IHttpClientFactory>(),
config["Stripe:ApiKey"]))
.Map("paypal", (in IServiceProvider sp) => new PayPalGateway(
sp.GetRequiredService<IHttpClientFactory>(),
config["PayPal:ClientId"],
config["PayPal:Secret"]))
.Map("square", (in IServiceProvider sp) => new SquareGateway(
sp.GetRequiredService<IHttpClientFactory>(),
config["Square:AccessToken"]))
.Build();
}
public IPaymentGateway GetGateway(string gatewayName, IServiceProvider serviceProvider)
{
if (!_factory.TryCreate(gatewayName, serviceProvider, out var gateway))
{
throw new NotSupportedException($"Payment gateway '{gatewayName}' is not supported");
}
return gateway;
}
public IEnumerable<string> SupportedGateways => new[] { "stripe", "paypal", "square" };
}
// Usage in controller
[HttpPost("checkout")]
public async Task<IActionResult> Checkout(
[FromBody] CheckoutRequest request,
[FromServices] PaymentGatewayFactory gatewayFactory,
[FromServices] IServiceProvider serviceProvider)
{
var gateway = gatewayFactory.GetGateway(request.PaymentMethod, serviceProvider);
var result = await gateway.ProcessAsync(new PaymentRequest
{
Amount = request.Amount,
Currency = request.Currency,
CardToken = request.CardToken,
OrderId = request.OrderId
}, HttpContext.RequestAborted);
if (result.Success)
return Ok(new { transactionId = result.TransactionId });
return BadRequest(new { error = result.ErrorMessage });
}
Why This Pattern
- Extensible: Add new gateways without modifying existing code
- Testable: Mock individual gateways for testing
- Configuration-driven: Gateway selection from request/config
Example 2: Report Generator Factory
The Problem
A reporting system needs to generate reports in multiple formats (PDF, Excel, CSV, HTML) with format-specific rendering logic.
The Solution
Use Factory to map format strings to report generators.
The Code
public interface IReportGenerator
{
byte[] Generate(ReportData data);
string ContentType { get; }
string FileExtension { get; }
}
public class ReportGeneratorFactory
{
private readonly Factory<string, ReportOptions, IReportGenerator> _factory;
public ReportGeneratorFactory()
{
_factory = Factory<string, ReportOptions, IReportGenerator>
.Create(StringComparer.OrdinalIgnoreCase)
.Map("pdf", (in ReportOptions opts) => new PdfReportGenerator(opts))
.Map("excel", (in ReportOptions opts) => new ExcelReportGenerator(opts))
.Map("xlsx", (in ReportOptions opts) => new ExcelReportGenerator(opts))
.Map("csv", (in ReportOptions opts) => new CsvReportGenerator(opts))
.Map("html", (in ReportOptions opts) => new HtmlReportGenerator(opts))
.Default((in ReportOptions opts) => new PdfReportGenerator(opts))
.Build();
}
public IReportGenerator GetGenerator(string format, ReportOptions options = null) =>
_factory.Create(format, options ?? new ReportOptions());
public bool IsFormatSupported(string format) =>
_factory.TryCreate(format, new ReportOptions(), out _);
}
public class PdfReportGenerator : IReportGenerator
{
private readonly ReportOptions _options;
public PdfReportGenerator(ReportOptions options) => _options = options;
public string ContentType => "application/pdf";
public string FileExtension => ".pdf";
public byte[] Generate(ReportData data)
{
using var doc = new PdfDocument();
var page = doc.AddPage();
var gfx = XGraphics.FromPdfPage(page);
// Render header
gfx.DrawString(data.Title, _options.TitleFont, XBrushes.Black, 50, 50);
// Render data table
var y = 100;
foreach (var row in data.Rows)
{
var x = 50;
foreach (var cell in row)
{
gfx.DrawString(cell.ToString(), _options.BodyFont, XBrushes.Black, x, y);
x += 100;
}
y += 20;
}
using var stream = new MemoryStream();
doc.Save(stream);
return stream.ToArray();
}
}
// Usage
var factory = new ReportGeneratorFactory();
var generator = factory.GetGenerator(request.Format, new ReportOptions
{
PageSize = PageSize.A4,
Orientation = Orientation.Landscape
});
var reportBytes = generator.Generate(reportData);
return File(reportBytes, generator.ContentType,
$"report-{DateTime.UtcNow:yyyyMMdd}{generator.FileExtension}");
Why This Pattern
- Format flexibility: Easy to add new formats
- Content negotiation: Select format based on request
- Encapsulated logic: Format-specific code isolated
Example 3: Database Provider Factory
The Problem
A multi-tenant application needs to support multiple database providers (SQL Server, PostgreSQL, MySQL, SQLite) per tenant configuration.
The Solution
Use Factory to create database connections based on provider type.
The Code
public interface IDatabaseProvider
{
IDbConnection CreateConnection(string connectionString);
string QuoteIdentifier(string identifier);
string GetLastInsertIdQuery();
}
public class DatabaseProviderFactory
{
private readonly Factory<string, IDatabaseProvider> _factory;
public DatabaseProviderFactory()
{
_factory = Factory<string, IDatabaseProvider>
.Create(StringComparer.OrdinalIgnoreCase)
.Map("sqlserver", static () => new SqlServerProvider())
.Map("mssql", static () => new SqlServerProvider())
.Map("postgresql", static () => new PostgreSqlProvider())
.Map("postgres", static () => new PostgreSqlProvider())
.Map("mysql", static () => new MySqlProvider())
.Map("mariadb", static () => new MySqlProvider())
.Map("sqlite", static () => new SqliteProvider())
.Build();
}
public IDatabaseProvider GetProvider(string providerName)
{
if (!_factory.TryCreate(providerName, out var provider))
{
throw new NotSupportedException(
$"Database provider '{providerName}' is not supported. " +
"Supported providers: sqlserver, postgresql, mysql, sqlite");
}
return provider;
}
}
public class SqlServerProvider : IDatabaseProvider
{
public IDbConnection CreateConnection(string connectionString) =>
new SqlConnection(connectionString);
public string QuoteIdentifier(string identifier) => $"[{identifier}]";
public string GetLastInsertIdQuery() => "SELECT SCOPE_IDENTITY()";
}
public class PostgreSqlProvider : IDatabaseProvider
{
public IDbConnection CreateConnection(string connectionString) =>
new NpgsqlConnection(connectionString);
public string QuoteIdentifier(string identifier) => $"\"{identifier}\"";
public string GetLastInsertIdQuery() => "RETURNING id";
}
// Multi-tenant usage
public class TenantDatabaseService
{
private readonly DatabaseProviderFactory _providerFactory;
private readonly ITenantConfigService _tenantConfig;
public TenantDatabaseService(
DatabaseProviderFactory providerFactory,
ITenantConfigService tenantConfig)
{
_providerFactory = providerFactory;
_tenantConfig = tenantConfig;
}
public IDbConnection GetConnection(string tenantId)
{
var config = _tenantConfig.GetConfiguration(tenantId);
var provider = _providerFactory.GetProvider(config.DatabaseProvider);
return provider.CreateConnection(config.ConnectionString);
}
}
Why This Pattern
- Multi-provider support: Tenants can use different databases
- Abstracted differences: Provider-specific SQL handled internally
- Easy testing: Mock providers for unit tests
Example 4: Notification Channel Factory
The Problem
A notification system needs to send alerts through multiple channels (Email, SMS, Slack, Push) with channel-specific formatting and delivery logic.
The Solution
Use Factory to create notification senders by channel type.
The Code
public interface INotificationChannel
{
Task SendAsync(Notification notification, CancellationToken ct);
bool CanHandle(NotificationType type);
}
public class NotificationChannelFactory
{
private readonly Factory<string, IServiceProvider, INotificationChannel> _factory;
public NotificationChannelFactory()
{
_factory = Factory<string, IServiceProvider, INotificationChannel>
.Create(StringComparer.OrdinalIgnoreCase)
.Map("email", (in IServiceProvider sp) => new EmailChannel(
sp.GetRequiredService<IEmailService>(),
sp.GetRequiredService<ITemplateEngine>()))
.Map("sms", (in IServiceProvider sp) => new SmsChannel(
sp.GetRequiredService<ISmsService>()))
.Map("slack", (in IServiceProvider sp) => new SlackChannel(
sp.GetRequiredService<ISlackClient>()))
.Map("push", (in IServiceProvider sp) => new PushChannel(
sp.GetRequiredService<IPushNotificationService>()))
.Map("webhook", (in IServiceProvider sp) => new WebhookChannel(
sp.GetRequiredService<IHttpClientFactory>()))
.Build();
}
public INotificationChannel GetChannel(string channelName, IServiceProvider sp)
{
if (!_factory.TryCreate(channelName, sp, out var channel))
{
throw new ArgumentException($"Unknown notification channel: {channelName}");
}
return channel;
}
public IEnumerable<INotificationChannel> GetChannelsForUser(
UserPreferences preferences,
IServiceProvider sp)
{
foreach (var channelName in preferences.EnabledChannels)
{
if (_factory.TryCreate(channelName, sp, out var channel))
{
yield return channel;
}
}
}
}
// Notification dispatcher
public class NotificationDispatcher
{
private readonly NotificationChannelFactory _channelFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IUserPreferencesService _preferences;
public async Task DispatchAsync(Notification notification, CancellationToken ct)
{
var userPrefs = await _preferences.GetAsync(notification.UserId, ct);
var channels = _channelFactory.GetChannelsForUser(userPrefs, _serviceProvider);
var tasks = channels
.Where(c => c.CanHandle(notification.Type))
.Select(c => c.SendAsync(notification, ct));
await Task.WhenAll(tasks);
}
}
// Usage
await dispatcher.DispatchAsync(new Notification
{
UserId = userId,
Type = NotificationType.Alert,
Title = "Server Alert",
Message = "CPU usage exceeded 90%",
Priority = Priority.High
}, cancellationToken);
Why This Pattern
- Channel flexibility: Easy to add new channels
- User preferences: Per-user channel selection
- Type filtering: Channels can filter by notification type
Key Takeaways
- Keyed registration: Map identifiers to implementations
- Custom comparers: Control key matching (case-sensitivity)
- Default fallback: Graceful handling of unknown keys
- TryCreate for probing: When missing keys are expected
- Immutable after build: Thread-safe lookup