Skip to Content
Course content

75: Practice Exercise: Building an Interface-Based Payment Gateway System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen in countless code reviews. A developer decides to use an interface to "do things the right way," but then they accidentally write code that completely defeats the purpose of that interface. Let's look at a snippet of a payment system that looks correct at first glance but is fundamentally broken from an architectural standpoint.

interface PaymentGateway {
    public function processPayment(float $amount): bool;
}

class StripeGateway implements PaymentGateway {
    public function processPayment(float $amount): bool {
        echo "Processing \${$amount} via Stripe...";
        return true;
    }
}

class PayPalGateway implements PaymentGateway {
    public function processPayment(float $amount): bool {
        echo "Processing \${$amount} via PayPal...";
        return true;
    }
}

class CheckoutService {
    private $gateway;

    // Here is the mistake
    public function __construct(StripeGateway $gateway) {
        $this->gateway = $gateway;
    }

    public function execute(float $amount) {
        return $this->gateway->processPayment($amount);
    }
}

$stripe = new StripeGateway();
$checkout = new CheckoutService($stripe);
$checkout->execute(100.00);

The Tight Coupling Trap

If you run the code above, it works perfectly. But here is the problem: we spent all that time defining a PaymentGateway interface, yet the CheckoutService specifically demands a StripeGateway in its constructor.

I call this "Interface Theater." We're pretending to use interfaces, but we've actually hard-coded our dependency. If the business decides to switch to PayPal tomorrow, we can't just swap the object passed into the constructor. We would have to go into the CheckoutService class and manually change the type-hint from StripeGateway to PayPalGateway. That is exactly the kind of manual, error-prone work interfaces are meant to eliminate.

Coding to the Interface, Not the Implementation

To fix this, we need to stop the CheckoutService from caring about which gateway is being used. It should only care that whatever object it receives is a PaymentGateway.

class CheckoutService {
    private $gateway;

    // Fix: Type-hint the interface, not the concrete class
    public function __construct(PaymentGateway $gateway) {
        $this->gateway = $gateway;
    }

    public function execute(float $amount) {
        return $this->gateway->processPayment($amount);
    }
}

// Now we can swap gateways without touching the CheckoutService code
$gateway = (rand(0, 1) == 0) ? new StripeGateway() : new PayPalGateway();
$checkout = new CheckoutService($gateway);
$checkout->execute(100.00);

By changing that one word in the constructor, we've decoupled the service from the implementation. The CheckoutService is now "blind" to the specific vendor; it just knows that any object passed to it is guaranteed to have a processPayment method. This is the core of the Dependency Inversion Principle.

Designing for Extensibility

When you're building these systems, I always recommend thinking about the "Third Vendor Scenario." If you have Stripe and PayPal, it's easy to think you can just use an if/else block. But what happens when you add Square, Apple Pay, and Braintree?

If you've coded to the interface, adding a new provider takes exactly two steps:

  • Create a new class that implements the interface.
  • Pass that new class into your service.
You never have to touch the logic inside your CheckoutService again. That's the difference between a codebase that feels like a house of cards and one that feels like a professional tool.


📋 Practical Task

Exercise: Building a Multi-Vendor Payment Orchestrator

Your goal is to build a flexible payment system that can handle multiple providers and a "logging" wrapper to track transactions.

Requirements:

  • Create a PaymentProvider interface with a method pay(float $amount): string.
  • Implement three concrete classes: StripeProvider, PayPalProvider, and SquareProvider. Each should return a unique string (e.g., "Paid $100 via Stripe").
  • Create a PaymentManager class. This class should have a constructor that accepts the PaymentProvider interface.
  • The PaymentManager should have a method called processOrder(float $amount) that calls the provider's pay method and returns the result.
  • The Challenge: Create a PaymentLogger class that also implements PaymentProvider. This class should take another PaymentProvider in its constructor (the "real" provider), call that provider's pay method, and then prepend "[LOG]: " to the resulting string before returning it.

Test your code by:

  1. Instantiating a SquareProvider.
  2. Wrapping that provider inside the PaymentLogger.
  3. Passing that logger into the PaymentManager.
  4. Calling processOrder(50.00) and ensuring the output is "[LOG]: Paid $50 via Square".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.