# Lazy Class

> A class does too little to justify the file, name and indirection it costs; fold it into its caller or delete it.

- kind: smell
- category: dispensables
- aliases: Freeloader
- canonical: https://aipatterns.dev/refactoring/smells/lazy-class
- inspired-by: https://refactoring.guru/smells/lazy-class

## Signs

- A class has one short method, or only forwards calls to another object.
- A subclass adds nothing over its parent, or overrides one trivial value.
- A class was planned for features that never arrived.
- After a refactoring, a class is left holding one field and a getter.
- Readers have to open the file to learn that it does almost nothing.

## Why it happens

Classes shrink over time as responsibilities move elsewhere, and nobody deletes the shell. Others are created up front for a design that was never built. Some come from a rule like "every concept gets its own class", applied to concepts that are a single value or a single line.

## Example

A billing module wraps a currency formatter in its own class and a subclass.

```ts title="billing.ts"
class MoneyFormatter {
  format(cents: number): string {
    return (cents / 100).toFixed(2);
  }
}

class UsdMoneyFormatter extends MoneyFormatter {
  format(cents: number): string {
    return "$" + super.format(cents);
  }
}

class InvoiceView {
  private formatter = new UsdMoneyFormatter();
  totalLine(totalCents: number): string {
    return `Total: ${this.formatter.format(totalCents)}`;
  }
}
```

Two classes and an inheritance link wrap one expression. Inline them:

```ts title="billing.ts"
const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });

class InvoiceView {
  totalLine(totalCents: number): string {
    return `Total: ${usd.format(totalCents / 100)}`;
  }
}
```

The platform formatter also handles thousands separators and negative values, which the hand-written class did not.

## Treatment

- If a class does little and has one main user, use [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) to move its members into that user.
- If a subclass or superclass adds almost nothing, use [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md) to merge it into its neighbor.
- If the class only forwards calls, treat it as [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md) and use [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md).
- If a standard library or platform feature already does the job, delete the class and call the platform directly.

## Payoff

- Fewer files and fewer jumps to follow the code.
- The logic sits next to its only caller, where readers expect it.
- Less surface to keep in sync during later refactorings.

## When to ignore

- The class marks a real boundary: an interface you mock in tests, a seam for a third-party dependency, or a public API type.
- It is a value type that gives a primitive a name and validation (for example `EmailAddress`). That cures [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md), even if it is small.
- The class is a deliberate placeholder for work scheduled in the current iteration, not "someday".
- Framework conventions require it (a controller, a migration, a DI module).

## Agent checklist

- [ ] You listed every user of the class and confirmed none needs it as a seam or mock point.
- [ ] The class is not a value object that protects an invariant.
- [ ] After inlining, no caller lost behavior, including edge cases the old class handled.
- [ ] Imports, DI registrations and exports referencing the class were removed.
- [ ] Tests pass and test doubles for the removed class were deleted or replaced.
