<arch.design/>
Principles/Abstraction
{ }CodeArchitecturebeginner1967oopinterfaceinformation-hidingmodelling

Abstraction

Reduce complexity by modelling only the details that matter for your problem — hide everything else behind a stable interface.

4/5
{ }
Operates at: Code level

Inside 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 · Rust
interface 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

Node.js streams

Readable and Writable abstract over files, HTTP, sockets, and in-memory buffers behind a common interface

AWS SDK

S3Client abstracts over HTTP multipart uploads, retries, and signing — callers just call putObject()

React

Component abstracts over DOM manipulation — render() describes what, React handles how

Industry adoption

4/5Widely adopted — mainstream at medium-to-large engineering orgs.

Related principles