OOP Series

Interfaces: The Contract You Must Keep

When you finish an Uber ride in New York, you pay.

Venmo, Zelle, credit card, Apple Pay — pick whatever you have. The app handles all of them.

Now think: does Uber’s engineering team know how Venmo’s servers process a transaction? Is Chase’s internal payment logic written somewhere in Uber’s codebase?

No.

Uber knows exactly one thing: “Tell me processPayment(amount) and I’ll handle the rest.” Venmo does it its way. Chase does it its way. Uber doesn’t care.

That contract is called an Interface.


1. What Is an Interface?

Imagine you’re starting a new job at a restaurant in the West Village. Your manager says: “You need to do three things: take orders, deliver food, collect payment. How you do it is up to you.”

That list of three things is an Interface. The manager is telling you what to do — not how to do it.

In programming, an Interface is a contract. It defines what methods a class must implement, but says nothing about what happens inside those methods.

from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        pass

    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        pass

    @abstractmethod
    def get_transaction_status(self, transaction_id: str) -> str:
        pass

PaymentGateway is saying: “Any class that implements me must have process_payment, refund, and get_transaction_status.” What they do inside? That’s the implementer’s business.

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#F5F5F5', 'primaryTextColor': '#000', 'primaryBorderColor': '#333', 'lineColor': '#333', 'background': '#fff'}}}%% graph TD classDef box fill:#F5F5F5,color:#000,stroke:#333 I["PaymentGateway Interface (Contract) process_payment() refund() get_transaction_status()"]:::box B["BkashPayment (own implementation)"]:::box N["NagadPayment (own implementation)"]:::box R["RocketPayment (own implementation)"]:::box C["CardPayment (own implementation)"]:::box I -->|"implements"| B I -->|"implements"| N I -->|"implements"| R I -->|"implements"| C

2. Core Properties of an Interface

What, not how. An interface holds method signatures, not implementations. Like a restaurant menu — it tells you what’s available, not how it’s cooked.

A class can implement multiple interfaces. A waiter can take orders, handle bills, and set tables. A class can honor multiple contracts at the same time.

You can’t instantiate an interface directly. The menu doesn’t cook the food — the chef does. You can’t create an object from an interface alone.

Loose coupling. Uber’s code doesn’t depend on Venmo. It depends on the PaymentGateway interface. If Venmo changes its internals tomorrow, Uber’s core code doesn’t need to change at all.


3. Code Example: Payment Gateway

from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        pass

    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        pass


class WalletPayment(PaymentGateway):
    def process_payment(self, amount: float) -> bool:
        print(f"Wallet: processing payment of {amount}...")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"Wallet: refunding transaction {transaction_id}...")
        return True


class DirectDebitPayment(PaymentGateway):
    def process_payment(self, amount: float) -> bool:
        print(f"Direct debit: processing payment of {amount}...")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"Direct debit: refunding transaction {transaction_id}...")
        return True


class RideCheckout:
    def __init__(self, payment_gateway: PaymentGateway):
        self.gateway = payment_gateway

    def complete_ride(self, fare: float):
        success = self.gateway.process_payment(fare)
        if success:
            print("Ride complete! Payment processed.")
        else:
            print("Payment failed.")

Now look at how independent RideCheckout is:

# With Venmo
checkout = RideCheckout(WalletPayment())
checkout.complete_ride(12.50)
# Wallet: processing payment of 12.5...
# Ride complete! Payment processed.

# With Zelle
checkout = RideCheckout(DirectDebitPayment())
checkout.complete_ride(12.50)
# Direct debit: processing payment of 12.5...
# Ride complete! Payment processed.

Not a single line of RideCheckout changed. Tomorrow if Apple Pay gets added, build an ApplePayPayment class. RideCheckout stays untouched.


Real-World Example: Notification Service

When an order is confirmed on DoorDash, the customer gets notified — SMS, email, and a push notification, all at once.

Three separate services. Three different company APIs. But the job is always the same: “Send this message to this user.”

from abc import ABC, abstractmethod
from typing import List

class NotificationService(ABC):
    @abstractmethod
    def send(self, recipient: str, message: str) -> bool:
        pass


class SmsNotification(NotificationService):
    def send(self, recipient: str, message: str) -> bool:
        print(f"SMS to {recipient}: {message}")
        return True


class EmailNotification(NotificationService):
    def send(self, recipient: str, message: str) -> bool:
        print(f"Email to {recipient}: {message}")
        return True


class PushNotification(NotificationService):
    def send(self, recipient: str, message: str) -> bool:
        print(f"Push notification to {recipient}: {message}")
        return True


class OrderProcessor:
    def __init__(self, notifiers: List[NotificationService]):
        self.notifiers = notifiers

    def confirm_order(self, user_id: str, order_id: str):
        message = f"Order #{order_id} confirmed!"
        for notifier in self.notifiers:
            notifier.send(user_id, message)
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#F5F5F5', 'primaryTextColor': '#000', 'primaryBorderColor': '#333', 'lineColor': '#333', 'background': '#fff'}}}%% graph LR classDef box fill:#F5F5F5,color:#000,stroke:#333 OP["OrderProcessor"]:::box NS["NotificationService Interface send()"]:::box SMS["SmsNotification"]:::box EM["EmailNotification"]:::box PN["PushNotification"]:::box OP -->|"depends on"| NS NS -->|"implements"| SMS NS -->|"implements"| EM NS -->|"implements"| PN
processor = OrderProcessor([
    SmsNotification(),
    EmailNotification(),
    PushNotification()
])

processor.confirm_order("+1-646-555-0192", "DD-8821")
# SMS to +1-646-555-0192: Order #DD-8821 confirmed!
# Email to +1-646-555-0192: Order #DD-8821 confirmed!
# Push notification to +1-646-555-0192: Order #DD-8821 confirmed!

Tomorrow if WhatsApp notifications get added, build a WhatsAppNotification class. OrderProcessor doesn’t change.


Summary

In the story In code
The restaurant manager’s job list Interface
The three tasks on that list Abstract methods
The waiter who does those tasks Implementing class
Uber’s payment contract PaymentGateway interface
Venmo and Chase doing it their own way Concrete implementations
Adding a new payment method easily Loose coupling

An interface says what to do. How to do it is the implementing class’s job.

Interfaces let you add new implementations without touching existing code.

A class can implement multiple interfaces — far more flexible than deep inheritance.


Next question: Should an object’s internal data be open to everyone? Shouldn’t some things stay hidden? That idea is called Encapsulation.

Next in the OOP series: Encapsulation — what doesn’t need to be seen, shouldn’t be