Abstraction
Reduce complexity by modelling only the details that matter for your problem — hide everything else behind a stable interface.
★★★★★4/5Inside a codebase — classes, modules, files
How it works
Abstraction is the practice of exposing a simplified model of something complex. In OOP, an abstraction defines what an object does without revealing how it does it. The caller only sees the interface — the method names, parameters, and return types — not the implementation behind them.
Abstract classes and interfaces are the main tools. An abstract Logger says 'I can log messages'; a ConsoleLogger or FileLogger fills in the how. The caller depends on the Logger abstraction and never knows (or needs to know) which concrete implementation is running.
Abstraction is not just a code technique — it's a thinking tool. Good abstractions model your domain at the right level of detail, making code read like the problem domain, not like implementation mechanics.
Implementation
TypeScript · Go · Rustinterface Logger {
info(message: string): void;
error(message: string, err?: unknown): void;
}
class ConsoleLogger implements Logger {
info(message: string) { console.log(`[INFO] ${message}`); }
error(message: string, err?: unknown) { console.error(`[ERROR] ${message}`, err); }
}
class SilentLogger implements Logger {
info() {}
error() {}
}
// Business logic depends only on the abstraction
async function processOrder(logger: Logger, orderId: string): Promise<void> {
logger.info(`Processing order ${orderId}`);
// ... order logic
logger.info(`Order ${orderId} complete`);
}
// Swap in a silent logger for tests — no console noise, same code path
await processOrder(new SilentLogger(), "ord_123");Why it matters
Without abstraction, callers are coupled to implementation details. Change the implementation and every caller breaks. A well-chosen abstraction lets the implementation change freely — and lets callers be written and understood in terms of the problem, not the solution.
✓ When to use
- →When multiple concrete implementations share the same concept (Logger, Cache, Repository)
- →When you want callers to be independent of implementation details
- →Modelling domain concepts — name things after what they are, not how they're stored
- →When testability matters — abstractions make mocking and test doubles natural
✗ When NOT to use
- →Premature abstraction before you have two concrete cases — YAGNI applies
- →Leaky abstractions that expose implementation details negate the benefit
Trade-offs
Callers are decoupled from implementation — swapping is safe
Wrong abstraction is worse than no abstraction — it misleads future developers
Code reads at domain level, not implementation level
Indirection makes tracing execution harder
Enables mocking and testing without real infrastructure
Too many thin abstractions add noise without adding value
In production
Readable and Writable abstract over files, HTTP, sockets, and in-memory buffers behind a common interface
S3Client abstracts over HTTP multipart uploads, retries, and signing — callers just call putObject()
Component abstracts over DOM manipulation — render() describes what, React handles how
Industry adoption
Related principles
Encapsulation
Bundle an object's data and the methods that operate on it into a single unit, and hide internal state behind a public interface.
Interface / Contract
Define what a component must do without dictating how it does it — so implementations can vary freely while callers remain stable.
Dependency Inversion Principle
High-level modules should not depend on low-level modules — both should depend on abstractions, inverting the conventional dependency direction.
Hexagonal Architecture
Place the domain at the centre and connect all I/O (HTTP, database, messaging) through explicit ports and adapters — so the core is framework-free and infinitely testable.