skip to Main Content

We need to dynamically support multiple payment methods (credit, paypal, google pay, etc…).
We can change the gateway we use to implement a certain payment method.
For example:
PayPal payment method can be implemented today by using Stripe SDK, and tomorrow by using PayPal SDK.

This lead me to the strategy pattern, where I can just implement each payment method by any gateway sdk.
Example: :

class PayPalModel : IPaymentModel

class PayPalGateway(val someSdk: SDK) : IPaymentService<PayPalModel> {
     override suspend fun authorizeFunds(model: PayPalModel) {
        // someSdk can be: stripe sdk, paypal sdk, ...
     }
}

and then simply inject the services, and call "authorizeFunds" :

val gateways =
        listOf(
            CreditGateway(),
            PayPalGateway()
        )
 val api = PaymentAPI(gateways)
 api.authorizeFunds(PayPalModel(...)

and then the "api" will internally be locating the correct service first :

private suspend fun findService(model: IPaymentModel): IPaymentService<IPaymentModel>? {
    for (service in paymentServices) {
        if (service.appliesTo(model)) {
            return service as IPaymentService<IPaymentModel>
        }
    }
    return null
}

However, for all our supported payment methods, upon placing an order the Client only sends the server two fields :

  1. The selected payment method type
  2. The auth id/token they received from the Stripe/Square/.. SDK

So in this case, it seems that the various payment models (PayPalModel, CreditModel,..) are redundant, because they will always contain the same data.

But, if we omit OR generalize this model, we will lose the type-safe distinction between all types of Gateways implementation.
For example, imagine:

class PayPalGateway : IPaymentService { // omiting the model
    override suspend fun authorizeFunds(model: PaymentRequest) {
      /// stripe sdk, paypal sdk, ...
    }
}
class CreditGateway : IPaymentService<PaymentRequest> { // generalizing the model
    override suspend fun authorizeFunds(model: PaymentRequest) {
      /// stripe sdk, paypal sdk, ...
    }
}

So except for the class naming, there is no real way to safely distinguish the services in order to use the correct one.
the function "findService" from above would be broken/useless.

What would be the wise choice to make sure that when the client sends me the method type + token/id, I pick the correct payment service?

2

Answers


  1. According to my humble experience in fintech, this case is usually solved by using enums. As simple as it is, I would extend the model with the property, representing the payment method/service.

    enum class PaymentType {
        PAY_PAL, CREDIT, ...
    }
    
    interface IPaymentModel {
        val type: PaymentType
    }
    

    I suppose, that client provides you with the information about their payment method/service. Since that, you can simply check the value and choose the correct gateway.

    when(model.type) {
        PAY_PAL -> // return the PayPal service class 
    }
    

    In my opinion, this approach is more robust than iterating over available services.

    Login or Signup to reply.
  2. In your scenario, maintaining a balance between type safety and the flexibility to switch payment gateways without redundancy is key. To achieve this, consider using a combination of the Factory and Strategy design patterns. Here’s an approach that might work for you:

    Define a Generic Payment Model: Instead of having different models for each payment method, define a generic PaymentRequest model that includes the payment method type and the auth token/id. This model can be extended if specific payment methods require additional data.

    Use a Payment Method Enum: Define an enum for payment methods (e.g., PayPal, Credit, GooglePay, etc.). This ensures type safety and makes it easier to manage different payment methods.

    Payment Service Interface: Keep your IPaymentService interface, but make it work with the generic PaymentRequest model. This interface can have a method like canHandle(paymentMethod: PaymentMethod): Boolean to determine if the service can handle a given payment method.

    Payment Service Factory: Implement a factory that is responsible for providing the correct IPaymentService implementation based on the payment method in the PaymentRequest. This factory uses the canHandle method of each service to determine the right one to use.

    Implement Specific Gateways: Each gateway (e.g., PayPalGateway, CreditGateway) implements the IPaymentService interface. In their implementation, they can specify which payment methods they can handle.

    Payment Processing API: Your PaymentAPI will use the factory to get the appropriate payment service and call authorizeFunds.

    For example:

    enum PaymentMethod {
        PAYPAL, CREDIT, GOOGLE_PAY // etc.
    }
    
    class PaymentRequest {
        PaymentMethod paymentMethod;
        String authToken;
        // other common fields
    }
    
    interface IPaymentService {
        boolean canHandle(PaymentMethod method);
        void authorizeFunds(PaymentRequest request);
        // other methods
    }
    
    class PaymentServiceFactory {
        public static IPaymentService getPaymentService(PaymentRequest request) {
            // return the appropriate service based on the request.paymentMethod
        }
    }
    
    class PayPalGateway implements IPaymentService {
        // specific implementation for PayPal
        public boolean canHandle(PaymentMethod method) {
            return method == PaymentMethod.PAYPAL;
        }
        public void authorizeFunds(PaymentRequest request) {
            // PayPal specific authorization
        }
    }
    

    // Similar implementations for other gateways…

    class PaymentAPI {
    public void authorizeFunds(PaymentRequest request) {
    IPaymentService service = PaymentServiceFactory.getPaymentService(request);
    service.authorizeFunds(request);
    }
    }

    In this design, the PaymentAPI does not need to worry about which service to use. The factory handles the logic of returning the correct service based on the payment method. This approach maintains type safety, reduces redundancy, and keeps the system flexible and scalable.

    Sorry if I made a syntax error, I am not great in Kotlin.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search