Interface / Contract
Define what a component must do without dictating how it does it — so implementations can vary freely while callers remain stable.
★★★★★5/5Inside a codebase — classes, modules, files
How it works
An interface (also called a contract or protocol) is a named set of method signatures. Any type that provides those methods satisfies the interface — it 'implements' the contract. The caller depends only on the interface, not on any concrete class.
This separation of 'what' from 'how' is the foundation of most software design principles. Dependency Injection works by injecting an interface. The Repository pattern exposes an interface. Hexagonal Architecture communicates through ports, which are interfaces. SOLID's Dependency Inversion Principle says high-level modules should depend on interfaces, not implementations.
Interfaces are a coordination primitive: a team can agree on an interface and implement it independently (database adapter, HTTP handler, ML model) — integration works as long as both sides honour the contract. Interfaces also make testing natural: swap the real implementation for a test double that implements the same interface.
Implementation
TypeScript · Go · Rustinterface Cache {
get(key: string): string | null;
set(key: string, value: string, ttlSeconds?: number): void;
delete(key: string): void;
}
// Production: Redis
class RedisCache implements Cache {
get(key: string) { /* Redis GET */ return null; }
set(key: string, value: string, ttl?: number) { /* Redis SET EX */ }
delete(key: string) { /* Redis DEL */ }
}
// Tests: in-memory — zero infrastructure, instant
class InMemoryCache implements Cache {
private store = new Map<string, string>();
get(key: string) { return this.store.get(key) ?? null; }
set(key: string, value: string) { this.store.set(key, value); }
delete(key: string) { this.store.delete(key); }
}
// Service depends on the contract — never on a concrete class
class UserService {
constructor(private cache: Cache) {}
async getUser(id: string): Promise<User | null> {
const hit = this.cache.get(`user:${id}`);
if (hit) return JSON.parse(hit);
const user = await db.findUser(id);
if (user) this.cache.set(`user:${id}`, JSON.stringify(user), 300);
return user;
}
}Why it matters
Code that depends on concrete classes is fragile — any change to that class ripples to all callers. Code that depends on an interface is stable — it keeps working as long as any conforming implementation exists, regardless of how many times the implementation changes internally.
✓ When to use
- →Any time you want the ability to swap implementations (production vs test, v1 vs v2)
- →Defining seams between teams or modules
- →Third-party integrations (payment, email, storage) — hide behind an interface
- →Effectively always — if a component has collaborators, those should be interfaces
✗ When NOT to use
- →Interfaces with a single implementation that will never vary add indirection for no benefit
- →Interfaces that leak implementation details (ISP violation) are harmful, not helpful
Trade-offs
Caller and implementation can evolve independently
Single-implementation interfaces add files and indirection with no benefit
Testing is natural — implement a test double in seconds
Interfaces must be designed carefully — wrong abstraction is costly to change
Multiple implementations can coexist and be selected at runtime
Interface discovery is harder in large codebases without good tooling
In production
io.Reader, io.Writer, http.Handler — tiny focused interfaces that compose the entire ecosystem
Repository<T>, Service, Component — every Spring bean is accessed through an interface in well-designed apps
Display, Iterator, From, Into — the standard library is built on trait contracts; your types plug in by implementing them
Industry adoption
Related principles
Abstraction
Reduce complexity by modelling only the details that matter for your problem — hide everything else behind a stable interface.
Dependency Inversion Principle
High-level modules should not depend on low-level modules — both should depend on abstractions, inverting the conventional dependency direction.
Dependency Injection
Supply a component's dependencies from the outside rather than letting it construct them — so implementations can be swapped without changing the component.
Interface Segregation Principle
Prefer many small, focused interfaces over one large general-purpose one — clients should not be forced to depend on methods they don't use.