advanced-manufacturing-techniques
Leveraging the Factory Pattern to Manage Different Payment Methods in Mobile Apps
Table of Contents
Managing multiple payment methods is one of the most challenging aspects of building a mobile application. Each payment method—whether it’s credit cards, PayPal, Apple Pay, Google Pay, or region-specific wallets like Alipay and WeChat Pay—comes with its own API, validation rules, error handling, and compliance requirements. Adding a new method often means touching multiple parts of the codebase, introducing risk and regression.
The factory pattern offers a structured solution to this complexity. By centralizing the creation of payment method objects, it decouples the client code from the concrete implementations of payments. This makes your app more maintainable, testable, and ready to scale as new payment options inevitably appear.
In this article, we will explore how to apply the factory pattern to manage different payment methods in mobile apps, with practical examples and best practices. We’ll also discuss how this pattern aligns with modern architectures like MVVM and Clean Architecture, and how you can integrate it with backend services such as Directus to store and retrieve payment configurations dynamically.
Understanding the Factory Pattern
The factory pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. In simpler terms, it encapsulates the instantiation logic so that clients don’t need to know the concrete classes; they only interact with a common interface.
This pattern is especially valuable in mobile apps where the set of payment methods can grow over time. Without a factory, you might end up with large if or switch statements scattered across your codebase, each one knowing how to construct a specific payment method. That approach violates the Open/Closed Principle and makes the code rigid—adding a new payment method requires modifying every place that creates one.
The factory pattern solves this by placing all creation logic in one place. When a new payment method is introduced, you simply add a new concrete class and update the factory method. The rest of your app remains unchanged.
How the Factory Pattern Works
The pattern typically involves three participants:
- Product – An interface or abstract class that defines the operations all payment methods must support (e.g.,
pay(amount),validate()). - Concrete Products – Classes that implement the product interface for each specific payment method (e.g.,
CreditCardPayment,PayPalPayment). - Creator (Factory) – A class (or function) that contains a method for creating products. The method accepts a parameter (e.g., a string or an enum) and returns the appropriate concrete product.
In mobile apps, this factory is often a singleton or a dependency-injected service, making it easy to swap implementations during testing or when supporting different app configurations.
Why Payment Methods Are Complex in Mobile Apps
Before diving into implementation, it’s helpful to understand the specific pain points that the factory pattern addresses. Payment handling in mobile apps goes beyond simply calling an API. You have to consider:
- Multiple SDKs and APIs – Each payment provider offers its own SDK or REST API. Integrating them directly into your business logic creates tight coupling.
- Regional and regulatory differences – A payment method available in one country may not be allowed in another. You may need to dynamically choose the factory configuration based on the user’s locale.
- Validation rules – Credit cards require Luhn checks and expiry date validation; digital wallets need token handling; local methods may enforce specific field requirements.
- Error handling and fallbacks – When a payment fails, the app may need to offer an alternative method or retry with different parameters. A centralized factory simplifies this logic.
- Testing – You don’t want to hit real payment servers during unit tests. The factory pattern allows you to inject mock payment objects easily.
- Dynamic configuration – Many apps fetch available payment methods from a remote server (for example, from Directus). The factory can interpret that configuration to create the right objects at runtime.
Given these challenges, a well-designed payment abstraction becomes critical. The factory pattern provides that abstraction layer.
Implementing the Factory Pattern for Payment Methods
Let’s walk through a concrete implementation. While the code examples are language-agnostic (pseudocode), we’ll use concepts that translate directly to Swift, Kotlin, Flutter, or React Native.
Step 1: Define the Payment Method Interface
Start by defining a common interface that all payment methods must implement. This interface should include methods that are universal across payment types, such as processPayment(amount: Double, currency: String) -> PaymentResult and validate() -> Bool.
interface PaymentMethod {
func processPayment(amount: Double, currency: String) -> PaymentResult
func validate() -> Bool
}
You can also include properties like displayName, iconUrl, or isAvailable, which the UI can use to show payment options.
Step 2: Create Concrete Payment Classes
For each payment method you support, create a class that implements the PaymentMethod interface. These classes encapsulate all the provider-specific logic.
class CreditCardPayment : PaymentMethod {
private let cardNumber: String
private let expiry: String
private let cvv: String
init(cardNumber: String, expiry: String, cvv: String) {
self.cardNumber = cardNumber
self.expiry = expiry
self.cvv = cvv
}
func validate() -> Bool {
// Luhn check, expiry date > today, etc.
return true // simplified
}
func processPayment(amount: Double, currency: String) -> PaymentResult {
// Call Stripe or Braintree SDK
return PaymentResult.success()
}
}
class PayPalPayment : PaymentMethod {
private let token: String
init(token: String) {
self.token = token
}
func validate() -> Bool {
return !token.isEmpty
}
func processPayment(amount: Double, currency: String) -> PaymentResult {
// Call PayPal SDK
return PaymentResult.success()
}
}
class ApplePayPayment : PaymentMethod {
private let paymentData: Data
init(paymentData: Data) {
self.paymentData = paymentData
}
func validate() -> Bool {
return paymentData.count > 0
}
func processPayment(amount: Double, currency: String) -> PaymentResult {
// Use PassKit or Stripe Apple Pay integration
return PaymentResult.success()
}
}
Notice that each concrete class handles its own validation and SDK calls. The rest of the app doesn’t care about the differences—it only calls processPayment().
Step 3: Build the Factory Class
The factory class is responsible for creating the appropriate payment method object based on input parameters. The input can come from user selection, a configuration object, or a server response (e.g., from Directus).
class PaymentFactory {
static func createPaymentMethod(type: String, parameters: [String: Any]) -> PaymentMethod? {
switch type {
case "credit_card":
guard let cardNumber = parameters["cardNumber"] as? String,
let expiry = parameters["expiry"] as? String,
let cvv = parameters["cvv"] as? String else {
return nil
}
return CreditCardPayment(cardNumber: cardNumber, expiry: expiry, cvv: cvv)
case "paypal":
guard let token = parameters["token"] as? String else {
return nil
}
return PayPalPayment(token: token)
case "apple_pay":
guard let paymentData = parameters["paymentData"] as? Data else {
return nil
}
return ApplePayPayment(paymentData: paymentData)
default:
return nil
}
}
}
In a more advanced scenario, you might use an enum instead of a string for the type, or you might load the factory configuration from a remote source. The key point is that the factory is the only place where concrete classes are instantiated.
Step 4: Using the Factory in Your App
Now, when a user selects a payment method and provides the necessary details, your view model or controller only needs to call the factory:
let paymentType = selectedPaymentMethod.type // "credit_card", "paypal", etc.
let parameters = collectInputParameters()
if let paymentMethod = PaymentFactory.createPaymentMethod(type: paymentType, parameters: parameters) {
paymentMethod.processPayment(amount: total, currency: "USD")
} else {
// show error – unsupported method or invalid input
}
This code is clean, testable, and open for extension. Adding a new payment method (e.g., Google Pay) requires only a new class and an additional case in the factory’s switch statement—no other changes are needed.
Handling Variations: Country-Specific and Dynamic Factories
In real-world apps, the set of available payment methods often changes based on the user’s country, the app version, or business rules. You can make the factory dynamic by feeding it a configuration object.
Suppose you use Directus to store enabled payment methods per region. Your backend returns a JSON object like this:
{
"methods": ["credit_card", "paypal", "apple_pay"],
"paypal": { "environment": "sandbox", "clientId": "abc123" }
}
You can store this configuration in a repository or in your app’s state. Then the factory can read it at runtime:
class DynamicPaymentFactory {
private let config: PaymentConfiguration
init(config: PaymentConfiguration) {
self.config = config
}
func createPaymentMethod(type: String, parameters: [String: Any]) -> PaymentMethod? {
guard config.methods.contains(type) else { return nil }
// Use config-specific parameters (e.g., clientId for PayPal)
// ... switch as before
}
}
This approach keeps your app adaptable without requiring a new app release for every payment provider change.
Benefits of Using the Factory Pattern
By now, the advantages should be clear. Let’s enumerate them more thoroughly.
1. Encapsulation of Object Creation
The factory centralizes the instantiation logic. If the constructor of a payment class changes (e.g., a new required parameter is added), you only update the factory and the concrete class. All callers remain unaffected.
2. Adherence to the Open/Closed Principle
Your code is open for extension (adding new payment methods) but closed for modification (existing classes don’t change). This reduces the risk of introducing bugs in already-tested payment flows.
3. Simplified Unit Testing
Because the factory can be mocked or replaced, you can easily inject fake payment objects during testing. For example, you can create a MockPaymentFactory that always returns a successful payment without touching the network.
4. Improved Code Organization
The factory pattern naturally groups related classes (payment methods) under a common interface. This makes the codebase easier to navigate and understand.
5. Runtime Flexibility
You can combine the factory with a strategy or template pattern to alter behavior based on runtime conditions—for instance, choosing between a test and production environment.
6. Scalability
As your app grows to support dozens of payment methods, the factory pattern scales gracefully. Each new method is a separate file, and the factory switch remains linear.
Real-World Considerations
Integration with Dependency Injection
In modern mobile architectures (MVVM, Clean Architecture, Viper), the factory should be registered in your dependency injection container. This way, you can inject a real factory in production and a mock factory in tests. For example, using Dagger in Android or Swinject in iOS:
// Swift with Swinject
container.register(PaymentFactoryProtocol.self) { _ in PaymentFactory() }
Error Handling and Fallbacks
Your factory should handle invalid input gracefully. Returning nil or throwing a specific error type allows the caller to present a meaningful UI. Similarly, you might implement a fallback factory that defaults to a universal payment method if the requested one fails to initialize.
Configuration from Backend (Directus)
Directus can be used to store payment method metadata, such as display order, required fields, or even custom validation rules. Your mobile app fetches this configuration on startup and passes it to the factory. This decouples the client from hardcoded payment logic.
For example, you could create a payment_methods collection in Directus with fields:
type(string: "credit_card", "paypal")display_name(string)required_parameters(JSON array of field names)is_active(boolean)
Your mobile app fetches this collection via the Directus SDK and builds the factory’s registry dynamically. This pattern allows non-developer stakeholders to control payment offerings without code changes.
Best Practices When Using the Factory Pattern
- Keep the factory simple. It should only create objects. If you find yourself adding business logic (e.g., currency conversion), extract that into separate services.
- Use strong typing. Prefer enums or sealed classes over strings for the method type. This prevents runtime errors from misspelled identifiers and gives you compiler support.
- Document the interface. Ensure that all members of the
PaymentMethodinterface have clear contracts. What doesvalidate()do exactly? DoesprocessPaymentthrow or return an error? - Test the factory separately. Write unit tests that verify the factory returns the correct concrete class for each input, and that it returns nil for unsupported types.
- Combine with other patterns. The factory works well with the Strategy pattern (to handle different payment flows) and the Builder pattern (if a payment method requires complex configuration).
- Consider a registry. Instead of a monolithic switch, you can build an open registry where payment method classes register themselves. This is common in plugin architectures.
Conclusion
Managing multiple payment methods in mobile apps doesn’t have to be a source of technical debt. By applying the factory pattern, you encapsulate the complexity of object creation, making your codebase cleaner, more testable, and ready for future expansion. The pattern respects the Open/Closed Principle, decouples business logic from third-party SDKs, and integrates smoothly with modern architectures and backend services like Directus.
When you next face a growing list of payment options, reach for the factory pattern. It will save you time, reduce bugs, and let you add new payment methods with confidence.
Further reading:
- Factory Method Pattern (Wikipedia)
- Stripe Payment Quickstart – mobile integration
- Directus Collections and API Reference