Facebook Pixel

Abstraction & Interfaces

When one part of a system depends on another, the question is always: how tightly are they coupled? If OrderService calls StripeAPI directly, it knows the name of every Stripe method, every parameter, every exception type. Add PayPal later and you rewrite OrderService. Add a test mock and you need to patch Stripe. The class has too many reasons to change.

Abstraction solves this by introducing a contract between the two sides. Instead of depending on StripeAPI, OrderService depends on a PaymentMethod interface — a minimal description of what a payment provider must be able to do. StripeAPI, PayPalClient, and a test FakePayment all implement that interface. OrderService never changes when a new provider is added.

The diagram below shows how OrderService depends on the interface, not on any concrete provider.

The payment method example

Consider an order checkout flow that needs to charge a customer. The naive implementation reaches directly into whichever payment library the company started with.

1# Bad: OrderService is hard-wired to Stripe.
2# Adding PayPal means modifying OrderService.
3# Unit-testing OrderService means making real Stripe network calls.
4class StripeAPI:
5    def charge_card(self, card_token: str, amount_cents: int) -> str:
6        # calls stripe.com/v1/charges ...
7        return "ch_stripe_abc123"
8
9class OrderService:
10    def __init__(self) -> None:
11        self._stripe = StripeAPI()   # hard dependency
12
13    def checkout(self, order: "Order", card_token: str) -> str:
14        charge_id = self._stripe.charge_card(card_token, order.total_cents())
15        order.mark_paid(charge_id)
16        return charge_id
1from abc import ABC, abstractmethod
2
3# Good: OrderService depends on the PaymentMethod interface, not any concrete provider.
4class PaymentMethod(ABC):
5    @abstractmethod
6    def charge(self, amount_cents: int, reference: str) -> str:
7        """Charge the customer and return a provider transaction id."""
8
9class StripePayment(PaymentMethod):
10    def charge(self, amount_cents: int, reference: str) -> str:
11        # calls stripe.com/v1/charges with amount_cents and reference
12        return f"ch_stripe_{reference}"
13
14class PayPalPayment(PaymentMethod):
15    def charge(self, amount_cents: int, reference: str) -> str:
16        # calls PayPal Orders API
17        return f"paypal_{reference}"
18
19class FakePayment(PaymentMethod):
20    """In-memory stub for unit tests — no network required."""
21    def __init__(self) -> None:
22        self.charged: list[tuple[int, str]] = []
23
24    def charge(self, amount_cents: int, reference: str) -> str:
25        self.charged.append((amount_cents, reference))
26        return f"fake_{reference}"
27
28class OrderService:
29    def __init__(self, payment: PaymentMethod) -> None:
30        self._payment = payment   # depends on the interface, not a concrete class
31
32    def checkout(self, order: "Order") -> str:
33        charge_id = self._payment.charge(order.total_cents(), order.id)
34        order.mark_paid(charge_id)
35        return charge_id

Structure of the abstraction

The relationship between OrderService and the payment providers now looks like this:


OrderService points at PaymentMethod, not at any provider. Adding a new provider is a new file that implements the interface — OrderService is untouched.

What the interface should contain

A good interface contains only what callers actually need. The PaymentMethod interface here has one method: charge. It does not expose refund, createCustomer, or any other provider capability — those are either not needed by OrderService or they belong on a different interface. An interface that grows without bound becomes as tightly coupled as a concrete class.

The right question when defining an interface is: what does the caller need, not what can the provider do? Starting from the calling side and working backward, OrderService needs to charge a customer and get a transaction id — that is exactly one method.

Interface vs abstract class

Both interfaces and abstract classes define a contract, but they differ in what they carry. An interface specifies only method signatures — no fields, no shared implementation. Any class can implement an interface regardless of where it sits in the class hierarchy, and a single class can implement many interfaces at once. PaymentMethod in this article is a capability contract: every class that implements it promises to provide charge. StripePayment, PayPalPayment, and FakePayment are otherwise unrelated; the only thing they share is the promise.

An abstract class can carry shared state and partial implementation. A base class AbstractPaymentMethod might hold a shared _logger field, or implement a charge_with_retry method that calls the abstract charge method three times before giving up. Subclasses inherit that behavior without re-implementing it. The tradeoff is that most languages allow a class to extend only one abstract class, so the hierarchy slot is spent.

The decision comes down to whether the relationship is "can do" or "is a kind of." Use an interface when multiple unrelated types all need to provide the same capability — payment providers, notification channels, storage backends. Use an abstract class when subclasses share real code or state, and the inheritance relationship reflects a genuine "is-a" where some behavior is always the same. In practice, a common pattern combines both: the interface defines the public contract, and an optional abstract class provides a partial implementation that concrete subclasses can extend if they share behavior.

In Python the distinction is softer because ABC with @abstractmethod serves as an abstract class, and a plain Protocol (structural typing) serves as an interface. In Java and TypeScript the split is explicit: interface vs abstract class.

Interview framing

In an LLD interview, the moment you hear "the system needs to support multiple X" — multiple payment providers, multiple notification channels, multiple storage backends — that is the signal to introduce an interface. Define the abstraction from the caller's perspective, keep it minimal, and inject the concrete implementation rather than creating it inside the class that uses it.

Interviewers also look for whether you can distinguish a good abstraction boundary from a leaky one. If the PaymentMethod interface exposed a chargeCard(card_token) method, it would be leaking Stripe's model — PayPal does not use card tokens in the same way. The correct interface is payment-provider-agnostic: a reference and an amount. That is the kind of judgment the question is probing.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro