Every Abstraction You Add Is a Debt Someone Else Has to Understand

by Arif Ikhsanudin, Backend Developer

The Cost That Doesn't Show in Your PR

Adding an abstraction layer feels like simplification. The calling code gets shorter. The implementation details disappear. The interface expresses intent at a higher level. Your PR looks cleaner.

What doesn't show in your PR is the bill: every developer who reads this codebase from now on has to understand the abstraction. They have to know what the abstraction does, where it's defined, what it hides, and when they need to look beneath it. That cost is paid repeatedly, by multiple people, across the lifetime of the codebase.

The abstraction you added in two hours may accumulate hundreds of hours of comprehension cost over a two-year codebase lifetime, depending on how many engineers encounter it and how often. When the abstraction is genuinely simplifying, this is a good trade. When it's adding indirection without reducing complexity, it's a hidden tax.

The Compounding Problem in Layered Systems

Individual abstractions are manageable. Systems with many abstraction layers compound the problem. In a layered architecture, a request might flow through:

HTTP Handler → Controller → Service → Repository → QueryBuilder → JDBC

Each layer is an abstraction. Each layer requires the developer to hold the contract of that layer in mind while reading the layer below it. Six layers means six mental models, all active simultaneously when debugging an issue that crosses layers.

This isn't an argument against layered architecture. It's an argument for keeping the number of layers proportional to the complexity they're actually managing. Three layers with clear responsibilities are better than six layers where the middle three exist primarily for theoretical flexibility.

What Justifies an Abstraction

The question to answer before adding an abstraction: what specific cognitive load am I removing, and does the cost of understanding the abstraction exceed the benefit of not knowing what it hides?

Justified abstraction: A PaymentProcessor interface with multiple implementations (Stripe, PayPal, Braintree) that are actually interchangeable in production. The caller genuinely doesn't need to know which provider is active. The abstraction removes real coupling between payment provider choice and the code that initiates payments.

Questionable abstraction: A Repository that wraps every database operation in a single codebase that has always used PostgreSQL and has no plans to change. The repository provides testability (legitimate), but also adds an indirection layer that every future developer has to traverse to understand what database operations are actually happening.

Unjustified abstraction: A StringHelper utility class with one method, used in one place, that wraps a trivially understandable string operation. This provides no benefit and adds one more file and one more layer of indirection to understand.

The Indirection Inversion

There's a useful distinction between abstraction that reduces complexity and indirection that merely relocates it. A well-named function that packages a complex algorithm is an abstraction — it hides complexity behind a clear interface and the complexity doesn't need to be understood unless the function is being modified. A factory that creates an object using five lines of code that could have been inline is indirection — the complexity hasn't been hidden, it's just in a different file.

Indirection forces the reader to navigate the codebase to follow the logic. In a large, active codebase, this navigation tax is significant. Every goto-definition that leads to another goto-definition is a tax on comprehension.

The Counter-Argument Worth Acknowledging

There are legitimate arguments for abstractions beyond immediate complexity reduction:

  • Testability: abstractions over external dependencies allow test doubles
  • Extension points: well-defined interfaces make adding new implementations easier
  • Boundary enforcement: explicit interfaces between modules prevent inadvertent coupling

These are valid. The point is not that abstractions are bad — it's that they have costs, and those costs should be weighed against the benefits explicitly. The default should not be "add an abstraction when in doubt." The default should be the simplest design that meets the requirements, with abstractions added when they provide specific, named benefits that outweigh their comprehension cost.

The Practical Takeaway

For the next abstraction you consider adding, write down: what specific complexity does this remove for the caller? What does the caller no longer need to know? Then estimate: how many times will a developer need to understand this abstraction over the next year? If the removal of complexity per encounter is less than the cost of understanding the abstraction, keep the concrete implementation. If it's greater, add the abstraction — and document what it hides and why that information is irrelevant to callers.

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

How Good Engineering Teams Use Code Review

Code reviews aren’t just a formality—they’re the secret sauce that separates good engineering teams from the rest. Done right, they improve code, knowledge, and culture.

Read more

Why Finnish Startups Hire Async Backend Contractors to Scale Beyond Helsinki's Small Talent Pool

Helsinki's engineering community is strong but small. The startups growing fastest have built a way to get backend work done that doesn't depend on the local pool being bigger than it is.

Read more

How to Set Clear Expectations Before Starting a Project

Nothing derails a project faster than mismatched expectations. Setting them clearly from the start saves time, stress, and headaches later.

Read more

Environment Variables in Docker Compose Without the Confusion

Docker Compose has multiple overlapping mechanisms for environment variables — .env files, environment blocks, env_file, shell variables — and they interact in ways that trip people up. Understanding the precedence order removes the guessing.

Read more