Back to the map

Module 20 · OOP fundamentals · Lesson 5 of 5

Abstraction and interfaces

Exposing what something does and hiding how. Interfaces as contracts, programming to an interface so implementations can be swapped and tested, nominal versus structural typing, and the leaky abstractions that give the game away.

CoreMust-knowMid 12 min Updated Nov 2026

Why it matters

Every system is too big to hold in your head, so you work with abstractions: a "storage" you can save to without knowing whether it is a database or a file, a "clock" that tells the time without you caring how. Good abstractions let teams work independently, let implementations be swapped, and make code testable with fakes. Bad ones leak details that callers come to depend on, and then nothing can change. Interfaces are the language feature that makes abstractions explicit.

The idea

Abstraction is choosing what to expose and what to hide: callers see the essential behaviour, the details stay behind it. An interface writes that choice down as a type: a named set of method signatures with no implementation, which any class can implement. Code that depends on the interface works with every implementation.

CheckoutServiceuses PaymentGatewaydepends on«interface»PaymentGateway: charge, refundStripeGatewayAdyenGatewayFakeGatewaytests
The service depends only on the interface. Production wires in the real gateway; tests wire in a fake; neither change touches the service.

An interface is a contract

Signatures are only part of the contract. The rest is behaviour you must document: what each method guarantees, what it does on failure, whether it is safe to call twice, whether it may block. "charge returns a result or throws PaymentDeclined; it is idempotent for the same idempotency key" is the contract; the method signature is just its outline. Every implementation must honour all of it, which is what the Liskov substitution principle in the Low-Level Design track formalises.

Program to an interface

Declare fields, parameters and return types as the interface, not the concrete class, and receive the implementation from outside (a constructor argument) rather than creating it inside. That is dependency injection in its plain form, and it is what makes code testable and swappable.

// Hard-wired: cannot test without the real gateway, cannot switch providers
class CheckoutService {
    private final StripeGateway gateway = new StripeGateway(System.getenv("STRIPE_KEY"));
}

// Programmed to an interface: any gateway, including a fake in tests
class CheckoutService {
    private final PaymentGateway gateway;
    CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
}

Not everything needs an interface. A class with one implementation that will never be swapped or faked gains nothing from one; introduce the interface when a second implementation or a test double actually appears.

Nominal and structural typing

In Java and C#, a class implements an interface only by declaring it (nominal typing). In Go and TypeScript, a type satisfies an interface by having the right methods, with no declaration (structural typing). Structural typing lets the consumer define a small interface for exactly what it needs, and existing types fit it automatically, which encourages small, focused interfaces. Python gets a similar effect through duck typing and Protocol types.

Leaky abstractions

Every abstraction hides something, and under pressure the hidden thing shows through. A remote call wrapped to look like a local method still fails with timeouts and still takes milliseconds. An ORM that hides SQL still generates slow queries. A "generic storage" interface still behaves differently on a database and on object storage. The defence is to put the unavoidable details into the contract (timeouts, failure modes, consistency) instead of pretending they do not exist.

Good abstractionLeaky or wrong abstraction
Named for what callers need: Clock, PaymentGateway, OrderRepositoryNamed for the implementation: MySqlHelper, StripeUtils
Small: the methods one client usesLarge: every method any client ever needed
Contract covers failure and costPretends a network call is a local call
Stable while implementations changeChanges whenever the implementation does

Trade-offs

Interfaces buy decoupling, swappability and testability, at the cost of an extra type and an indirection that makes code slightly harder to navigate. Too many single-implementation interfaces add noise without benefit; too few leave classes welded to concrete dependencies. Introduce them at real boundaries: external services, storage, time, randomness, and anything you need to fake in tests.

In an interview

State the boundary: "OrderService depends on a PaymentGateway interface and an OrderRepository interface, injected through the constructor, so we can switch payment providers and unit-test the service with fakes." Then mention the contract: "charge is idempotent per key and throws a typed exception on decline, which both implementations honour."

Check yourself

A service creates a concrete MySqlUserStore inside its constructor. Unit tests are slow and need a running database. What is the minimal design change that fixes this?

Abstraction and interfaces · Backplane