Skip to main content

Trade-offadvanced

Abstraction vs. Complexity

Overview

Abstraction exists to reduce what has to be understood at a time. When it works, it is the most powerful tool in software engineering. When it does not, it adds complexity instead of hiding it — and the result is worse than not having it.

The difference is measurable:

real axis how much this abstraction hides, divided by how much it
requires you to understand in order to use it

Ousterhout calls this depth: a deep abstraction has a small interface over a substantial implementation. A shallow one has an interface as complex as what it hides — and in that case the layer is pure cost.

Problem

The reflex to organize into layers produces structures that look right and do not pay for themselves:

Controller → Service → ServiceImpl → Repository → RepositoryImpl → Mapper → DAO

Seven layers in which four merely forward. Adding a field requires touching seven files, and none of the seven hides anything from anyone — whoever reads has to cross all of them to understand what happens.

The symmetric error is the absence of abstraction: business logic mixed with data access, with transport and with formatting, in long functions where nothing has a name.

Both produce the same symptom — difficulty in understanding — and for that reason they are frequently confused. The answer to the second is to abstract; to the first, to remove a layer.

Core Concepts

Depth: what it hides over what it requires

deep small interface, substantial implementation
e.g.: a file-reading function that handles buffering,
encoding, partial error and end of file
shallow interface as large as what it hides
e.g.: a class whose only method calls another method
with the same parameters

The practical test: how many parameters and concepts do I need to understand to use this, compared to what I would need to understand without it? If the ratio is not clearly favorable, the layer does not pay for itself.

See abstraction.

A leaky abstraction charges twice

closed abstraction use it without knowing what is underneath
leaky abstraction you have to know both levels

When the underlying behavior crosses the interface — performance, a specific error, a limit, transaction semantics — the user has to understand the abstraction and what it hides. The cost doubles instead of reducing.

Common examples:

object-relational mapper that generates bad queries → you have to know SQL
HTTP client that hides timeouts → you have to know the network
queue abstracted over different semantics → you have to know the mechanism

That does not condemn abstraction — it means the comparison must be made counting the real cost, which includes the lower level.

A small interface is not a simplistic interface

small few concepts, few parameters, predictable behavior
simplistic hides what the user needs to decide, and they end up
working around the abstraction

An abstraction that hides something essential forces the workaround — and code that works around the abstraction is the worst state: you pay for the layer and use the lower level anyway.

A worthless layer is recognizable

methods that merely forward, with the same signature
one implementation per interface, for years
classes whose name is the type's, not the responsibility's
a simple change requiring touching four files in sequence
whoever reads has to cross the layer to understand anything

Three or more of these signs indicate a removable layer. Removing is one of the highest-return refactorings and one of the least practiced, because layers look like virtue.

The cost falls on the reader, and it recurs

writing the abstraction once
reading it every time, by everyone

A badly chosen abstraction charges every person who enters the code, forever. That is why "I understand it, it's simple" is not an argument — the question is whether someone arriving in two years will understand it without an explanation.

See complexity.

Naming is the cheapest form of abstracting

extract a function with a name that describes the intent
cost: nearly zero
benefit: the reader does not need to understand the body

This is frequently enough, and it is what you should try before any interface, layer or hierarchy. A large part of the complexity attributed to a lack of abstraction is, in fact, a lack of names.

Signs of the wrong choice

abstracted too much
forwarding layers
deep hierarchies with one real path
interfaces with one implementer
navigation requiring crossing 4+ files to find the logic
new people taking a long time to find where things happen

abstracted too little
long functions mixing levels — business, data, transport
the same handling block repeated in twenty places
an infrastructure detail appearing in business logic
impossible to test the rule without a database and a network

Cost of changing your mind

concrete → abstract cheap: the cases exist, the extraction is local
abstract → concrete expensive: removing a layer used by many requires
mapping dependents, and fear freezes it

The asymmetry is the same one in simplicity vs. flexibility, with the same conclusion: when in doubt, start concrete. Abstracting later is done with information; removing an abstraction is done with uncertainty.

Mental Model

Does it hide more than it requires you to understand? If not, the layer is cost. And before abstracting, try naming.

When to Use

Abstract when:

  • The implementation is substantial and the interface can be small.
  • There is more than one real case, with known variation.
  • The abstraction closes — the user does not need the lower level.
  • The name expresses intent, not mechanism.
  • What is hidden is volatile and what is exposed is stable.

Prefer concrete when:

  • The layer would merely forward.
  • There is only one case.
  • The abstraction would leak essential behavior.
  • Naming a function already solves it.
  • The structure is dictated by convention and not by need.

When Not to Use

By layering convention, without each one hiding something.

With a single implementer, indefinitely.

When it leaks what the user needs to know.

Before the second case. This floor is lower than the rule of three, and the difference is one of operation: there you decide whether to unify fragments that repeat, and the third case exists to reveal the axis of variation; here you decide whether to hide an implementation behind an interface, and the second case is already enough because the interface does not have to accommodate any variation — it has to hide something volatile. If the abstraction also unifies duplication, the floor over there applies.

To look organized — visual organization is not abstraction.

Alternatives

  • Naming — extract a function with an intent name; the cheapest.
  • Module with a boundary, no interface — grouping without indirection.
  • Composition instead of hierarchy — avoids inheritance depth.
  • Flatten — remove the layer and accept the concrete where it is clear.

The last is the most underused: removing a forwarding layer improves readability and performance at the same time, and is almost never proposed.

Trade-offs

AbstractionConcrete
Hides detailDirect to read
Reuse and substitutionNo indirection
Cost of understanding the layerRepetition possible
Hard to removeEasy to abstract later
Small interfaceComplete interface
Easy to useDoes not force workarounds
May hide the essentialMore concepts
DeepShallow if it exposes everything

Failure Modes

Forwarding layer. Cost without benefit.

Leaky abstraction. Two levels to understand instead of one.

Simplistic interface. Forces workarounds; you pay for the layer and do not use it.

Deep hierarchy. Expensive navigation to find the logic.

Abstraction before the second case. Shape of the first.

Absence of names. Complexity attributed to a lack of structure.

Common Mistakes

Creating a layer by architectural convention, without asking what it hides.

Measuring quality by the number of interfaces.

Not considering removing a layer as a refactoring.

Abstracting before naming.

Ignoring that abstractions leak when comparing costs.

Real-World Example

A healthcare company adopted a standardized layered architecture for all its services, defined by an internal guide. Each entity required:

Controller
Input and output DTO
DTO Mapper
Service Interface
Service Implementation
Repository Interface
Repository Implementation
Entity
Entity Mapper

Nine artifacts per entity. A measurement over 41 entities in three services:

interfaces with a single implementer 78 of 82
methods that merely forward, with no logic ~64% of the total
files touched in a simple field change average of 6.8
average time for a new person to locate
where a business rule lives ~25 min, measured in
an exercise with 6 people
business rules in the Service layer present in 12 of 41 cases

The last line is the diagnosis. In 29 of the 41 entities, the service layer contained no logic — it existed because the guide required it.

And the side effect: since the standard structure did not accommodate the logic that actually existed, rules ended up in the controllers and in the mappers, which is where there was room.

The revision of the guide:

A layer is mandatory only when it hides something. The written rule: a layer must answer "what do I not need to know because of it?". If the answer is "nothing", it is not created.

Interface only with a second implementer — or with a concrete substitution need in testing that is not met another way.

Simple entities with no service layer. The controller talks to the repository directly when there is no rule.

A real domain layer for the 12 entities with substantive logic, this time designed from the rules and not from the template. See tactical design.

Name before structuring as explicit guidance in the guide, with examples.

Flattening of the 29 entities with no logic, done incrementally over seven months.

Results:

artifacts per simple entity from 9 to 4
interfaces with one implementer 9 (all justified)
files touched in a field change average of 2.3
time to locate a business rule ~6 min
business rules in a controller or mapper 0
lines of code in the three services -31%
test coverage unchanged

The data point the team highlights: business rules stopped leaking into the controllers after the layers were reduced. The excessive structure was not protecting the domain; it was pushing the logic wherever it fit.

In retrospect: the original guide had been written with good intentions — standardize to make reading across services easier. It standardized the form and not the substance, and form without substance is exactly what Ousterhout calls a shallow layer: reading cost for everyone, benefit for no one.

Practical Exercise

Take a layer in your system and answer: what do I not need to know because of it?

If you cannot answer, or if the answer is "nothing", it is a candidate for flattening.

Interview Questions

  • How do you measure whether an abstraction pays for itself?
  • Why does a leaky abstraction cost more than the absence of abstraction?
  • Why does naming usually solve what is attributed to a lack of structure?

Further Reading

  • Ousterhout, John. A Philosophy of Software Design. 2nd ed. Yaknyam Press, 2021.
  • Spolsky, Joel. The Law of Leaky Abstractions. 2002.
  • Parnas, David. On the Criteria To Be Used in Decomposing Systems into Modules. CACM, 1972.
Finished reading this document?Your progress is saved in this browser only.