Building an e-commerce API is straightforward until you need to support multiple payment gateways like Stripe and Paymob. The naive approach is to use endless if-else or switch statements inside your controllers to determine which payment service to call. This violates the Open/Closed Principle and makes the codebase tightly coupled and very hard to maintain.
In this article, I will show you how I used the Factory Design Pattern in C# to resolve and manage multiple payment gateways, keeping my .NET Core application scalable and clean.
- The Contract (Interface): IPaymentGateway First, I need a unified contract. Every payment gateway I integrate must implement this interface. This ensures my core application doesn't care about the specific implementation details of Paymob or Stripe.
public interface IPaymentGateway
{
string GatewayName { get; }
string Currency { get; }
Task<APIResponse<PaymentResultDTO>> CreatePaymentIntentionAsync(UnifiedPaymentRequestDTO request);
Task<APIResponse<string>> ProcessWebhookAsync(HttpRequest request);
}
- The Implementations Next, I implement this interface for my specific providers. Notice how each class defines its own unique GatewayName.
Paymob Implementation:
public class PaymobGateway : IPaymentGateway
{
public string GatewayName => "paymob";
public string Currency => "EGP";
// Dependencies like HttpClient, DbContext, and Settings injected here
public async Task<APIResponse<PaymentResultDTO>> CreatePaymentIntentionAsync(UnifiedPaymentRequestDTO request)
{
// Paymob specific logic to generate payment intent and URL
// ...
return APIResponse<PaymentResultDTO>.Success(resultDto, "Payment intent created successfully");
}
public async Task<APIResponse<string>> ProcessWebhookAsync(HttpRequest request)
{
// Paymob specific HMAC verification and webhook processing
// ...
return APIResponse<string>.Success($"Order {order.Id} Paid Successfully!");
}
}
Stripe Implementation:
public class StripeGateway : IPaymentGateway
{
public string GatewayName => "stripe";
public string Currency => "USD";
public async Task<APIResponse<PaymentResultDTO>> CreatePaymentIntentionAsync(UnifiedPaymentRequestDTO request)
{
// Stripe specific logic to generate payment intent
// ...
return APIResponse<PaymentResultDTO>.Success(new PaymentResultDTO
{
GatewayName = "Stripe",
PaymentUrl = session.Url
}, "Stripe Session Created");
}
public Task<APIResponse<string>> ProcessWebhookAsync(HttpRequest request)
{
// Stripe specific webhook processing
// ...
return APIResponse<string>.Success("Webhook processed successfully");
}
}
- The Factory: Resolving Dependencies Dynamically This is where the magic happens. I create a Factory class that takes an IEnumerable. ASP.NET Core's Dependency Injection will inject all registered gateways into this list.
The Factory uses LINQ to match the requested gateway name with the GatewayName property of the injected services, ignoring case sensitivity for safety.
public interface IPaymentGatewayFactory
{
IPaymentGateway GetGateway(string gatewayName);
}
public class PaymentGatewayFactory : IPaymentGatewayFactory
{
private readonly IEnumerable<IPaymentGateway> _gateways;
private readonly ILogger<PaymentGatewayFactory> _logger;
public PaymentGatewayFactory(IEnumerable<IPaymentGateway> gateways, ILogger<PaymentGatewayFactory> logger)
{
_gateways = gateways;
_logger = logger;
}
public IPaymentGateway GetGateway(string gatewayName)
{
var gateway = _gateways.FirstOrDefault(g =>
g.GatewayName.Equals(gatewayName, StringComparison.OrdinalIgnoreCase));
if (gateway == null)
{
_logger.LogError($"Gateway Name '{gatewayName}' Not Found");
throw new ArgumentException($"Payment gateway '{gatewayName}' is not supported.");
}
return gateway;
}
}
- Wiring it up in Dependency Injection (Program.cs) To make this work, I register multiple implementations for the same interface IPaymentGateway, and then register my Factory.
// Register multiple gateways
builder.Services.AddScoped<IPaymentGateway, PaymobGateway>();
builder.Services.AddScoped<IPaymentGateway, StripeGateway>();
// Register the factory
builder.Services.AddScoped<IPaymentGatewayFactory, PaymentGatewayFactory>();
- Using it in the Controller Now, our Controller is completely decoupled. It doesn't know anything about Paymob or Stripe. It just asks the factory for the requested gateway and executes the payment!
[ApiController]
[Route("api/[controller]")]
public class CheckoutController : ControllerBase
{
private readonly IPaymentGatewayFactory _gatewayFactory;
public CheckoutController(IPaymentGatewayFactory gatewayFactory)
{
_gatewayFactory = gatewayFactory;
}
[HttpPost("pay")]
public async Task<IActionResult> ProcessPayment([FromQuery] string provider, [FromBody] UnifiedPaymentRequestDTO request)
{
// 1. Get the correct gateway based on the user's choice
var gateway = _gatewayFactory.GetGateway(provider);
// 2. Process the payment
var result = await gateway.CreatePaymentIntentionAsync(request);
return Ok(result);
}
}
Conclusion
By taking advanatage of the Factory Pattern and ASP.NET Core's built-in Dependency Injection, I built a scalable payment module. If i want to add PayPalGateway tomorrow or another gateway, I just create the class and register it in Program.cs without modifying the Controller or the Factory. This adheres to the Open/Closed Principle.
Check out the complete source code, including the real-time Webhook handling and database transactions, on my GitHub:
https://github.com/elshwilahmed/PaymentGateways-API
Top comments (0)