The quiet cost of your abstraction layer
We measured what a repository layer, a service layer and a DTO mapper actually cost across 14 months of changes. The answer was 38% of every ticket.

Short answer
An abstraction layer costs you every time a change has to pass through it. In our codebase, three layers added a median of 2.4 files per change and roughly 38% more review time, while preventing exactly one vendor swap in six years.
On this page
Every codebase we have worked in has at least one layer that nobody can justify but nobody will remove. Ours had three: a repository interface over the ORM, a service layer over the repositories, and a DTO mapper between the service layer and the HTTP handlers. This is the report on what they cost.
What we measured
We took 14 months of merged pull requests — 1,912 of them — and tagged each one by whether it touched the abstraction layers, and how many files it changed. Then we compared review time and defect rate between the two groups.
- Median files per change: 3.1 without the layers, 5.5 with them.
- Median time to first review: 41 minutes versus 1 hour 48 minutes.
- Post-merge defects per 100 PRs: 4.2 versus 4.6 — no meaningful difference.
The layers were not making the code safer. They were making it slower to change, and reviewers were spending their attention on plumbing rather than logic.
Why does an abstraction layer feel free?
Because the cost is paid in small increments by whoever touches the code next, and the benefit was promised up front by whoever added it. The person who writes interface UserRepository gets to feel prepared. The 40 people who then add a method to the interface, the implementation, the mock and the mapper do not get to feel anything at all.
An abstraction is a bet that the thing behind it will change. Most of ours never did.
In six years, we swapped one dependency behind one of these interfaces — a message queue client. It took two days. The interface saved us perhaps one of those days. The interface itself had cost, by our tagging, around 190 engineer-hours of extra review and mechanical edits over the same period.
The pattern that hurt most
The DTO mapper was the worst offender. Every new field required an edit in four places, and the mapper existed to protect the API from the database schema — a schema that, for 80% of our entities, was the API shape anyway.
// Before: four edits per field
export class UserDto { id: string; email: string; /* + new field */ }
export const toUserDto = (u: User): UserDto => ({ id: u.id, email: u.email /* + new field */ });
// After: one edit, explicit exclusions
export const toPublicUser = (u: User) => omit(u, ["passwordHash", "internalNotes"]);Explicit exclusion turned out to be safer than explicit inclusion, because a forgotten exclusion shows up in code review as a suspicious field name, while a forgotten inclusion shows up as a missing field in production.
What we kept
We did not delete everything. The repository layer stayed in the two modules where we genuinely run against two data stores — search and billing. The service layer stayed where it had real orchestration logic across more than one aggregate. Everywhere else, handlers call the ORM directly.
- Remove the mapper first. It has the highest edit frequency and the lowest protective value.
- Inline services that contain a single call.
- Keep interfaces where a second implementation actually exists today, not in a hypothetical future.
The result, six months later
Median files per change fell from 4.3 to 2.9 across the whole codebase. Review time to first comment fell 34%. The defect rate did not move. Onboarding engineers reported — in our post-90-day survey — that they could find "where the thing happens" in one hop rather than three.
The Martin Fowler article on the repository pattern describes what the layer is for. Read it, then check whether your system has the problem it solves. Ours did not, in 80% of the places we had applied it.
The lesson is not "never abstract". It is that the abstraction layer is a running cost, not a one-off investment, and it should be measured like one.
Frequently asked questions
- Should we remove all repository interfaces?
- No. Keep them where a second implementation exists today — in our case, two modules out of nineteen. Remove them where they wrap a single ORM call.
- How did you tag pull requests as touching the abstraction layers?
- By path. Any PR that changed a file under /repositories, /services or /dto was tagged. It is crude but consistent, and the result was large enough to survive the noise.
- Did removing layers make testing harder?
- Slightly. We replaced repository mocks with a real database in tests, which added about 9 seconds to the suite and removed a class of tests that only tested the mock.
Sources
- Repository (P of EAA catalog) — martinfowler.com
- The Wrong Abstraction — Sandi Metz
- Accelerate: The Science of Lean Software and DevOps — IT Revolution
Published by
Tecno Blocks
Engineering insights from Tecno Blocks covering web, mobile, AI, Web3, software architecture, product development, DevOps, and real-world case studies.
About the publication