Abstractions Are Powerful Until They Hide Too Much

by Arif Ikhsanudin, Backend Developer

The Abstraction That Worked Until It Didn't

A team builds a DataService abstraction that handles all database interactions. It's clean, the callers don't know what database is underneath, and adding new query types is easy. Then they start hitting performance problems. A query that should use an index isn't. A caching layer is causing stale reads in one context. The abstraction hides exactly the information needed to diagnose and fix the problem.

The developers must now pierce the abstraction — understand what the DataService actually does under specific conditions — to fix a production issue. The abstraction was valuable during development. It is costing them during operations.

This is not an argument against abstractions. It is an argument that abstractions leak, and the question is whether they leak in ways that matter.

What Abstractions Do Well

Abstractions hide irrelevant complexity. A developer calling emailService.sendConfirmation(order) doesn't need to know whether the service uses SendGrid, SES, or Postfix. The abstraction is valid because the underlying mechanism genuinely doesn't matter to the caller — all implementations satisfy the contract equivalently.

Good abstractions reduce the amount of context required to work at a given level. They make code at the caller level shorter, cleaner, and more expressive. They allow implementation details to change without forcing changes to every caller.

When Abstractions Hide Too Much

Joel Spolsky's "Law of Leaky Abstractions" (2002) identifies the core problem: abstractions are built on top of specific implementations, and the behavior of those implementations bleeds through the abstraction in ways that violate the model.

A network socket abstracted as a file descriptor leaks when the network is unreliable. An ORM abstracted as "just work with objects" leaks when query performance matters. A distributed cache abstracted as "just an in-memory map" leaks when consistency matters.

The problem isn't that these abstractions are wrong. It's that they create a gap between the mental model the abstraction implies and the actual behavior of the underlying system. When that gap matters — under load, during failures, in edge cases — the developer must either understand the underlying system anyway, or produce incorrect diagnoses and fixes.

The Specific Signs of Over-Abstraction

Debugging requires reading the implementation of the abstraction: If understanding a bug requires looking inside the abstraction rather than at its interface, the abstraction is hiding too much. Good abstractions can be debugged from their interfaces.

Configuration options expose implementation details: An abstraction with a maxConnections parameter, a retryPolicy setting, and a consistencyLevel option is an abstraction that knows its users will need to understand the underlying system. This is not necessarily wrong — it may be the right tradeoff — but it signals that the abstraction is leaky.

Performance is unpredictable from the abstraction alone: If callers can't reason about the performance characteristics of an operation from its interface — if they need to know whether it makes a network call, how many database queries it triggers, whether it uses a cache — the abstraction is hiding information that the caller needs.

Testing requires mocking the abstraction at the implementation level: Test doubles that need to know about internal behavior to produce realistic test scenarios indicate that the abstraction is not providing adequate isolation.

Designing Better Abstractions

The key question when building an abstraction: what information do callers genuinely not need to know, and what information do they need to reason about correctness and performance?

For error handling: callers almost always need to know about failure modes. An abstraction that catches and swallows exceptions is hiding information the caller needs to respond appropriately.

For performance: callers need to know whether an operation is O(1) or O(n), whether it makes a network call, whether it's cacheable. These don't need to be exposed as implementation details — but the abstraction's documentation or interface design should communicate them.

For consistency: callers need to know whether reads are strongly consistent or eventually consistent. An abstraction that hides this produces incorrect application behavior when consistency matters.

The Practical Takeaway

For each significant abstraction in your codebase, ask: under what conditions does the underlying implementation's behavior become visible to callers? Document those conditions in the abstraction's interface — in method signatures where possible, in documentation where necessary. If the conditions are so numerous that they consume most of the documentation, the abstraction may be more leaky than useful, and a thinner wrapper that exposes more of the underlying system may serve better.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Integration Tests Are Not Just Bigger Unit Tests

Integration tests and unit tests answer different questions. Treating integration tests as unit tests that cover more lines leads to slow, brittle suites that provide neither the speed of unit tests nor the coverage of true end-to-end tests.

Read more

Message Queues vs Direct API Calls — A Decision Guide With Real Trade-offs

The choice between publishing to a message queue and calling a downstream API directly determines your system's failure boundary — and getting it wrong in either direction creates either over-engineering or brittle coupling.

Read more

Hiring a Generalist vs a Specialist Backend Developer — What Actually Matters

The generalist vs specialist debate in backend hiring is really a question about where your system complexity lives — and the wrong hire relative to your actual complexity creates either over-engineering or operational risk.

Read more

How to Build and Push Docker Images Automatically in Your Pipeline

Automating Docker builds in CI means handling authentication, tagging, multi-platform targets, and registry configuration correctly. The moving pieces fit together cleanly once you understand what each one does.

Read more