Implementing Payment Provider Abstraction with Feature Flags

When integrating multiple payment providers into an application, managing the transition and allowing for flexibility becomes crucial. This post explores how to introduce an abstraction layer combined with feature flags to handle different payment providers, such as Stripe and Paddle.

The Challenge

Directly integrating a payment gateway like Stripe can tightly couple the payment logic within the application. Introducing another provider, such as Paddle, without proper abstraction can lead to complex conditional logic and increased maintenance overhead. We need a mechanism to switch between providers seamlessly, ideally controlled by a feature flag.

The Solution: Abstraction and Feature Flags

We introduce a PaymentProviderInterface to define a contract for all payment providers. This interface outlines the common methods required, such as processing payments, handling subscriptions, and managing refunds.

interface PaymentProviderInterface {
    public function processPayment(float $amount, string $currency, array $options): string;
    public function subscribe(string $planId, array $options): string;
    public function refundPayment(string $transactionId, float $amount): bool;
}

Concrete implementations for each provider (e.g., StripePaymentProvider, PaddlePaymentProvider) then implement this interface.

class StripePaymentProvider implements PaymentProviderInterface {
    public function processPayment(float $amount, string $currency, array $options): string {
        // Stripe specific logic here
        return 'stripe_transaction_id';
    }

    public function subscribe(string $planId, array $options): string {
        // Stripe subscription logic
        return 'stripe_subscription_id';
    }

    public function refundPayment(string $transactionId, float $amount): bool {
        // Stripe refund logic
        return true;
    }
}

class PaddlePaymentProvider implements PaymentProviderInterface {
    public function processPayment(float $amount, string $currency, array $options): string {
        throw new RuntimeException('Paddle not yet implemented');
    }

    public function subscribe(string $planId, array $options): string {
         throw new RuntimeException('Paddle not yet implemented');
    }

    public function refundPayment(string $transactionId, float $amount): bool {
         throw new RuntimeException('Paddle not yet implemented');
    }
}

A factory or dependency injection container is used to resolve the correct provider based on a feature flag. The feature flag can be managed via an admin panel or a configuration file. This allows to toggle payment providers without modifying the core application code.

class PaymentProcessor {
    private PaymentProviderInterface $paymentProvider;

    public function __construct(PaymentProviderInterface $paymentProvider)
    {
        $this->paymentProvider = $paymentProvider;
    }

    public function processOrder(Order $order): string
    {
        return $this->paymentProvider->processPayment($order->getTotal(), $order->getCurrency(), []);
    }
}

// Example of how a payment provider could be selected
$paymentProvider = FeatureFlag::isEnabled('use_paddle_payment_provider')
    ? new PaddlePaymentProvider()
    : new StripePaymentProvider();

$processor = new PaymentProcessor($paymentProvider);
$transactionId = $processor->processOrder($order);

Benefits

  • Decoupling: The application code is decoupled from specific payment provider implementations.
  • Flexibility: Switching between payment providers is simplified and can be controlled via feature flags.
  • Testability: Each payment provider can be tested independently.
  • Gradual Rollout: New payment providers can be introduced gradually using feature flags, minimizing risks.

Conclusion

By combining an abstraction layer with feature flags, managing multiple payment providers becomes more manageable and less prone to errors. This approach enables a smooth transition between providers, allows for A/B testing, and provides the flexibility needed to adapt to changing business requirements. Remember to thoroughly test each provider implementation and ensure proper error handling for a robust payment processing system.

Gerardo Ruiz

Gerardo Ruiz

Author

Share: