---
title: "Shallow Abstraction"
type: "ai-smell"
slug: "shallow-abstraction"
url: "http://localhost:3000/en/ai-smells/shallow-abstraction.md"
category: "Structure"
description: "An AI assistant wraps code in extra functions, classes, or interfaces that add a layer of indirection without hiding any complexity or enabling reuse — abstractions that merely forward to a single call site."
---
# Shallow Abstraction

> An AI assistant wraps code in extra functions, classes, or interfaces that add a layer of indirection without hiding any complexity or enabling reuse — abstractions that merely forward to a single call site.

## Signs and Symptoms

A reviewer recognizes Shallow Abstraction when a new function/class/interface adds a _name and a layer_ but no _leverage_: its body restates its signature, it forwards straight through to one underlying call, it is invoked exactly once, or it hides no decision, invariant, or variation. The code _looks_ layered and "enterprise-grade," but you have to read through every layer to understand anything — the abstraction encapsulates nothing.

Tell-tale signs:

* Pass-through functions/methods whose body is a single delegating call with no added behavior.
* Wrapper classes (`FooService` → `FooRepository` → `db`) where each layer just calls the next.
* Single-implementation interfaces (`IClock` with only `SystemClock`) introduced speculatively "for testability" with no second impl or fake.
* `utils`/`helpers` functions used in exactly one place.
* Redundant aliasing/renaming that forwards a value unchanged.

This is the classic **Middle Man** / **Lazy Class** / **Speculative Generality** smell, and the flip side of OX Security's "over-specification" finding (hyper-specific, single-use solutions instead of generalizable components).

```ts
// AI added three "layers" that each forward to the next.
function getUserName(id: string) {
  return fetchUserName(id);            // adds nothing
}
function fetchUserName(id: string) {
  return userRepository.getName(id);   // adds nothing
}
class UserRepository {
  getName(id: string) {
    return db.query("SELECT name FROM users WHERE id = ?", [id]);
  }
}
// getUserName is the only call site, used once.
// Two of the three layers add zero behavior — pure Middle Man.

```

## Reasons for the Problem

**Why models produce it**

* **Imitating the _shape_ of good architecture.** Training corpora are saturated with tutorials and enterprise scaffolding — service/repository/factory layers, DI, one-impl interfaces. The model pattern-matches the _ceremony_ of abstraction without the substance, because a named helper is locally plausible and "looks professional." Next-token generation optimizes for resembling well-structured code, not for whether a layer earns its keep.
* **No repo context.** Generating locally, the model can't see that the helper is called once, or that a canonical abstraction already exists elsewhere — so it invents fresh shallow layers instead of reusing them. This is exactly GitClear's measured trend: refactored ("moved") lines fell from \~24% (2020) to under 10% (2024) while copy/pasted lines rose and code clones grew \~4x. Models add structure but never _consolidate_ it.
* **By-the-book fixation + over-specification.** OX Security found AI over-specifies in 80–90% of generated code, producing "narrow solutions that cannot be reused," and avoids refactors in 80–90% ("stops at good enough"). Shallow wrappers are the visible residue.
* **Prompt-literalism and sycophancy.** Told to "add a service layer" or "make it clean/modular," the model adds layers literally even when YAGNI applies — the over-engineering / "god agent" tendency practitioners describe in the _code-smells-for-AI-agents_ discussion.
* **Training-cutoff staleness.** It will wrap a deprecated API in a thin adapter rather than migrate off it.

**Why it hurts**

* **Maintainability & review load.** Every change must thread through dead layers; readers inline-read through Middle Men. OX's "Army of Juniors" point: reviewers can't keep pace with code that _looks_ structured but has no real seams.
* **Correctness & security.** arXiv 2509.20491 and SonarSource both note that aliasing and wrappers "obscure the decisive call sites," degrading data/control-flow and taint analysis — a wrapper can silently swallow an error or hide a sink from a scanner.
* **Tech-debt accrual.** Speculative one-impl interfaces ossify; and because each shallow abstraction is single-use, the _next_ variation gets copy-pasted instead of parameterized — directly feeding the clone growth GitClear measured. You pay for the indirection without ever collecting the reuse it promised.

## Treatment

**Review & prompting tactics**

* Apply the **Rule of Three**: "Don't add a function/class/interface unless it's used in ≥2 places _or_ hides a real decision/invariant. Inline single-use helpers (YAGNI)."
* Force reuse: "Before adding a helper/service/interface, search the repo for an existing one and reuse it." Point the model at the canonical module.
* Make it justify each layer: "List every new function/class and state what variation or invariant it hides; delete any that just forward."
* Require it to **run the linter** (`no-useless-constructor`, `no-useless-rename`) and a **duplication scan** (`jscpd`) and fix what they flag.

**Refactorings (name them):** **Inline Function**, **Inline Class**, **Remove Middle Man** for pass-throughs; **Collapse Hierarchy** / drop the interface for single-impl interfaces. When duplication is the _real_ driver, do **Extract Function once** into a genuinely shared, parameterized helper — not a wrapper per call site.

Before — Middle Man:

```ts
function getUserName(id: string) { return fetchUserName(id); }
function fetchUserName(id: string) { return userRepository.getName(id); }

```

After — Inline Function / Remove Middle Man (one call site, no behavior added):

```ts
const name = userRepository.getName(id);

```

Before — Speculative one-impl interface:

```ts
interface IClock { now(): number; }
class SystemClock implements IClock { now() { return Date.now(); } }

```

After — Collapse Hierarchy (use the concrete type; reintroduce the interface only when a second impl, e.g. a test fake, actually exists):

```ts
class SystemClock { now() { return Date.now(); } }

```

## Detected by

- **ESLint** `no-useless-constructor` — Pass-through constructor that only delegates to super or is empty (https://eslint.org/docs/latest/rules/no-useless-constructor)
- **ESLint** `no-useless-rename` — Redundant import/export/destructuring aliasing that forwards a name unchanged (https://eslint.org/docs/latest/rules/no-useless-rename)
- **PMD (Java)** `UselessOverridingMethod` — Overriding method that merely calls super with the same arguments (https://pmd.github.io/pmd/pmd_rules_java_design.html)
- **SonarSource (Sonar S4144)** `Functions/methods should not have identical implementations` — Flags duplicated single-use abstractions with identical bodies (https://rules.sonarsource.com/java/RSPEC-4144/)
- **jscpd** `duplication threshold` — Detects the near-duplicate single-use helpers that shallow abstractions fail to consolidate (https://github.com/kucherenko/jscpd)
