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

Lisbon Is No Longer the Affordable Tech Hub It Used to Be — Here Is What Startups Do Now

Lisbon built its reputation as a place where startups could hire well without spending like San Francisco. That window has mostly closed.

Read more

The Evolving Role of a Tech Lead With Modern Tools

Modern development tools are transforming how tech leads do their work. From code review automation to team collaboration, the role is shifting—but not disappearing.

Read more

Fixed Price vs Time & Materials — Which Contract Model Works Better for Backend Projects

Fixed price contracts transfer risk to the contractor and invite scope games; time and materials contracts transfer risk to the client and require active oversight — understanding which risk you are better positioned to manage determines which model to use.

Read more

Why Cheap Freelancing Can Damage Your Career

Ever wonder why some freelancers seem stuck forever in low-paying gigs? Cheap freelancing isn’t just about money—it can quietly sabotage your career.

Read more