Skip to main content
CleanCodeMastery

Lazy Class: The Watchman Whose Only Job Is Pressing One Lift Button

Learn the Lazy Class code smell with a society watchman story. Find classes that do too little to deserve existing, and cure them with Inline Class.

20 min read Updated June 11, 2026beginner
code-smellsdispensableslazy-classinline-classcollapse-hierarchyrefactoringtypescriptcsharp

🛗 The watchman who presses one button

Green Park Society in Kolkata has a story every resident tells with a laugh. It is about Bhola kaka, the lift watchman.

Years ago, the society's lift was old and moody. It needed a special trick: you had to hold the ground-floor button for five full seconds, or the lift would get stuck between floors with a groan that the whole building could hear. Visitors kept getting trapped. Children did it for fun. The fire brigade came twice. So the society committee, in an emergency meeting full of loud opinions and cold tea, created a special post: a lift watchman. Bhola kaka's whole job was to sit by the lift on a wooden stool and press the button correctly for people. It made complete sense — at that time. He saved dozens of visitors from getting stuck. He was, honestly, a hero with one finger.

Then, three years ago, the society installed a brand-new automatic lift. No tricks needed. You press the button; the lift comes. A child can do it. A child does do it, daily, forty times, for fun.

But here is the funny part: the lift watchman post still exists. Every month, the society pays Bhola kaka's salary. He sits on his stool beside the lift, reading the newspaper. When a visitor walks up, he leans over and... presses the button for them. The same button they would have pressed themselves, half a second later. He does nothing else. No security checking, no register, no gate duty. One button, once in a while.

The new treasurer, Mrs. Bose, looked at the accounts and asked the obvious question at the annual general meeting: "Why are we paying a full salary for a job that takes one finger?" There was a long silence. Somebody said, "But he has always been there." Somebody else said, "What if the old lift problem comes back?" Mrs. Bose pointed out that the old lift was sold for scrap three years ago. More silence. The post made sense once. Now it is just a cost that everyone forgot to remove.

Figure 1: A visitor's trip through the lobby — what exactly did the watchman add?

In code, we have the same character: a class that was created for a good reason long ago, but today does almost nothing. It still has a salary — a file, a name, an import, a jump for every reader — but its whole body is one tiny pass-through. This is the Lazy Class smell.

🤔 What is this smell?

A Lazy Class (also called a Freeloader) is a class that does too little to justify the cost of existing. It might hold one trivial method, just forward calls to another class, or wrap a single field with no rules. Whatever useful work it once did has shrunk to nearly nothing, but the class itself remains.

Every class in your program charges a fixed fee, no matter how little it does:

  • It is a name every team member must learn and remember.
  • It is a file readers must open and a jump they must follow.
  • It is one more box in every diagram, one more import, one more thing to keep mentally separate.

A hardworking class pays this fee easily — it gives you a meaningful unit of responsibility in return. A lazy class pays nothing back. The reader jumps into its file expecting substance, finds a one-line pass-through, and jumps back out. Attention spent, nothing learned. Exactly like walking up to the watchman and watching him press the button you could have pressed.

💡

The deciding question for any class is never "is this class small?" It is: "does this class earn the cost of being separate?" A small class with a real job — validation, a safety-giving type, a true boundary — earns its keep. A small class that is just a shell does not. Judge by earnings, not by size.

College corner: Object-oriented metrics give this intuition numbers. A lazy class typically scores near zero on weighted methods per class (WMC) and contributes nothing to cohesion measures like LCOM, while still adding edges to the dependency graph — meaning it increases coupling surface without adding responsibility. In Fowler's economic framing, every abstraction has a comprehension cost, and abstraction is only profitable when it compresses more complexity than it adds. A lazy class is negative-profit abstraction: full indirection cost, zero compression. This is also why "number of classes" is a meaningless quality metric on its own — ten meaningful classes beat thirty boxes where twenty are shells.

Here is the whole territory in one map:

Figure 2: The full map of the Lazy Class smell

🔍 How to spot it

Walk through your codebase with this checklist:

  • A class with a single trivial method, or only a constructor and getters, that adds nothing a plain field could not.
  • A subclass that overrides nothing meaningful and adds no behavior of its own.
  • A class created for growth that never came ("we will surely add more here later").
  • A class whose whole body could be pasted into its only caller with zero loss of clarity.
  • An interface plus exactly one implementation, created "for flexibility", with no second implementation in sight after years.
  • A class that only receives a call and forwards it to another class unchanged (its cousin smell: Middle Man).
SignQuestion to askIf the answer is...
One tiny methodCould this be a method on its caller?Yes → lazy
Wrapper around one fieldDoes the wrapper validate, format, or protect anything?No → lazy
Subclass adds nothingDoes it override or extend any behavior?No → lazy
Interface with one implementationIs there a real test seam or published boundary today?No → lazy
Class shrunk by refactoringDid its responsibilities move elsewhere?Yes → leftover shell
"Manager"/"Helper" with 10 linesDoes it hold a real concept or just float?Floats → lazy

The single best mental tool for judging a class is to place it on this chart — how much does it do, and how widely is it used?

Figure 3: Judging a class by work done and usage — the bottom-left corner is the watchman's stool

⚠️ Why it is a problem

You might think: "It is small, it is harmless, let it be." Here is why that is wrong.

Cost 1: The indirection tax. Every reader who meets the class must open it to learn it is empty. One lazy class wastes a minute. Forty lazy classes waste a minute each, for every new team member, forever. The watchman's salary is paid monthly; the lazy class's salary is paid per reader.

Cost 2: The design signal gets diluted. In a healthy codebase, "this is a class" means "this is a meaningful unit of responsibility". When half the classes are inert shells, that signal dies. Readers can no longer trust that a class matters, so they must inspect everything. The truly important abstractions drown in the noise of trivial ones.

Cost 3: Maintenance drag. Lazy classes still ride along in every rename, every dependency upgrade, every API migration. You pay to keep them compiling, and they deliver nothing for the payment. (This is the same drag that Dead Code causes — the two smells are close relatives.)

Cost 4: They breed. One pass-through class makes the next one look normal. New developers copy the house style: "Oh, here we wrap everything in a XyzManager." Soon the architecture diagram has twice the boxes and half the meaning.

Here is what the indirection tax looks like for a new teammate trying to answer one simple question — "how does the lift move?":

Figure 4: A new developer pays the indirection tax, one empty file at a time

And the tax compounds as shells accumulate:

Figure 5: Time for a newcomer to trace one feature, as lazy classes pile up

The life story of a lazy class explains why nobody notices the problem while it forms:

Figure 6: The life of a class — useful once, hollow now, deleted at last
Figure 7: The two endings — pay the tax forever, or inline the shell

💻 A real-life code example

Let us write Green Park Society's lift system in TypeScript — watchman included.

// Smelly version: spot the freeloaders
class LiftButton {
  constructor(private readonly floor: number) {}
  press(): number {
    return this.floor; // that's it. that's the whole class.
  }
}
 
// The "watchman": exists only to press the button for you
class LiftWatchman {
  constructor(private readonly button: LiftButton) {}
  assistVisitor(): number {
    return this.button.press(); // forwards the call, adds nothing
  }
}
 
// A subclass that adds... nothing
class AutomaticLift extends Lift {
  // empty. it was going to have "smart scheduling" someday.
}
 
class Lift {
  private currentFloor = 0;
 
  goTo(floor: number): void {
    this.currentFloor = floor;
    console.log(`Lift moving to floor ${floor}`);
  }
}
 
// caller
const button = new LiftButton(0);
const watchman = new LiftWatchman(button);
const lift = new AutomaticLift();
lift.goTo(watchman.assistVisitor());

Count the freeloaders:

  1. LiftButton wraps a single number and returns it. No validation (a negative floor? floor 99 in a 4-floor building? — it does not care). It is a number in a costume.
  2. LiftWatchman receives a call and forwards it unchanged. This is the literal watchman pressing the button you could press. Pure Middle Man behavior, which is laziness in delegation form.
  3. AutomaticLift is an empty subclass kept for a "smart scheduling" feature that never came — laziness born from Speculative Generality.

To understand the four-line operation "move the lift to floor 0", a reader must visit four classes. The society is paying three extra salaries. Here is the payroll, in class-diagram form:

Figure 8: The payroll before cleanup — three of the four classes do no real work

If you audited a typical mature codebase for this smell, the suspicious classes usually fall into these buckets:

Figure 9: Where lazy classes come from in real projects

🧹 Cleaning it up, step by step

Step 1: Confirm the laziness. For each suspicious class, check: does it validate anything? Transform anything? Protect anything? Could a second implementation realistically appear? For all three classes here, the answer is no.

Step 2: Retire the watchman with Inline Class. Move LiftWatchman's one job into the caller, then delete the class. Do the same for LiftButton — its "value" is just a number; pass the number.

Step 3: Merge the empty hierarchy with Collapse Hierarchy. AutomaticLift adds nothing over Lift, so the two layers become one. (If only a tiny pass-through method remains somewhere, Inline Method finishes the job.)

Step 4: Delete and enjoy.

// Clean version: one class, one real responsibility
class Lift {
  private currentFloor = 0;
 
  goTo(floor: number): void {
    this.currentFloor = floor;
    console.log(`Lift moving to floor ${floor}`);
  }
}
 
// caller
const lift = new Lift();
lift.goTo(0);

Four classes became one. Nothing of value was lost — because the other three never held value. The reader's journey shrank from four files to one.

Step 5 (important): know when to STOP deleting. Suppose LiftButton had been this instead:

// NOT lazy: this wrapper earns its keep through validation
class FloorNumber {
  constructor(private readonly value: number) {
    if (!Number.isInteger(value) || value < 0 || value > 4) {
      throw new Error(`Green Park has floors 0 to 4, got ${value}`);
    }
  }
  toNumber(): number {
    return this.value;
  }
}

This class is small, but it is working. It guarantees that an invalid floor can never exist anywhere in the program. That guarantee is a real salary-worthy job. The smell is the emptiness, never the smallness. On the quadrant chart in Figure 3, FloorNumber sits comfortably in the "keep it" zone, while LiftWatchman sits in the inline corner.

Figure 10: Deciding the fate of a suspiciously small class

🟦 The same smell in C#

A shipping module after two years of refactoring. The helper used to compute taxes, handle currencies, and round totals. All of that moved elsewhere. What remains:

// Before: the hollowed-out leftover of past refactorings
public class PriceHelper
{
    public decimal AddDelivery(decimal price) => price + 50m;
}
 
public class Checkout
{
    private readonly PriceHelper _helper = new();
 
    public decimal FinalAmount(decimal cartTotal)
        => _helper.AddDelivery(cartTotal);
}

A whole class, file, and instantiation for + 50m. Apply Inline Class:

// After: the shell is gone; the knowledge stays, with a name
public class Checkout
{
    private const decimal DeliveryCharge = 50m;
 
    public decimal FinalAmount(decimal cartTotal)
        => cartTotal + DeliveryCharge;
}

One file fewer, one jump fewer, and the delivery charge is still named clearly. If delivery pricing ever becomes genuinely complex — weight slabs, pin-code zones, partner rates — a DeliveryPricing class will earn its way back in. Classes should be hired for real work, not kept on old sympathy.

A Python example, since the freeloader speaks every language:

# Before: a class with one method that wraps one expression
class GstCalculator:
    def add_gst(self, amount: float) -> float:
        return amount * 1.18
 
# every caller:
total = GstCalculator().add_gst(bill)
 
# After: a plain function does this job honestly
GST_RATE = 0.18
 
def add_gst(amount: float) -> float:
    return amount * (1 + GST_RATE)
 
total = add_gst(bill)

College corner: Notice the language nuance here. In Java and C#, the unit of organization is the class, so laziness usually means inline into another class. In Python, TypeScript, and Go, module-level functions are first-class citizens — so the cure for a lazy class is often demotion to a function, which is even cheaper. A useful cross-language principle: choose the smallest unit of organization that does the job — expression, function, class, module, service — and only climb a level when the current one genuinely overflows. Many lazy classes exist simply because someone started one level too high.

🏢 Where this smell hides in real projects

  • Residue of big refactorings. Methods get pulled up, features get removed, and the once-busy class becomes a shell. Nobody deletes it because "it has always been there". Lazy classes are most often fossils of change — exactly like Bhola kaka's post surviving the lift it was created for.
  • One-implementation interfaces everywhere. Some codebases auto-create IFooService for every FooService by habit. Where no test seam or boundary is actually used, each pair is one lazy layer. (Modern mocking tools can often mock concrete classes anyway.)
  • Exception classes with empty bodies. Forty custom exceptions that add no fields, no message, no special handling — only a name. A handful with real meaning would serve better.
  • "Manager", "Processor", "Helper" shells. Created because the architecture template demanded a layer, holding one ten-line method that belongs in a neighboring class.
  • Empty subclasses for "future variants". PremiumUser extends User with an empty body, waiting three years for premium features. This is Speculative Generality leaving lazy fossils behind.
  • Wrapper-of-a-wrapper layers. OrderFacade calls OrderManager calls OrderService calls the repository. Often only one of those layers does real work.

⚖️ When it is okay to ignore

Honesty time. Small classes are sometimes the best design, and deleting them would be vandalism. Here is the judgment table.

Small classKeep or remove?Why
Value object that validates on constructionKeepIt guards an invariant; safety is real work
Distinct ID types (CustomerId vs OrderId)KeepThe type system now prevents mix-up bugs
Interface with a real test seam used todayKeepFaking a hard dependency is a concrete benefit
Published extension point with external usersKeepOutsiders depend on it; removing breaks them
Class that is small today but actively growingKeep, watchIt is a seedling, not a fossil
Hollowed-out leftover of old refactoringsRemoveA shell teaches nothing and costs a jump
Empty subclass for an imagined futureRemoveAdd it back when the future actually arrives
Pure pass-through wrapperRemoveMiddle Man laziness; inline it
⚠️

Before deleting any class, check who uses it. A class that looks unused inside your project may be public API for someone outside it — another team, a plugin, a published library consumer. Search published contracts first, delete second. Inside your own application code, though, be brave: version control remembers everything.

🛠️ Which refactorings cure it

SituationCuring refactoringResult
Class with few members, one main userInline ClassMembers move into the user; shell deleted
Near-empty subclass or superclassCollapse HierarchyTwo hierarchy levels merge into one
Tiny pass-through method left behindInline MethodThe indirection disappears
Class only delegates to anotherRemove Middle ManCallers talk to the real worker directly
Wrapper that should grow insteadAdd real behavior (Move Method into it)The lazy class becomes a working value object

Note the last row: sometimes the cure for a lazy class is not deletion but promotion. If PostalCode is a lazy string-in-a-box, you can either inline it away — or give it the validation job it always deserved. Both cures end the laziness. Mrs. Bose's committee had the same two options: retire the watchman post, or make it a real job — gate register, parcel handling, visitor passes. What they could not justify was paying full salary for one finger.

📦 Quick revision box

+--------------------------------------------------------------+
|  LAZY CLASS — QUICK REVISION                                 |
+--------------------------------------------------------------+
|  Story   : A watchman post kept alive only to press one      |
|            lift button anyone could press themselves.        |
|  Smell   : A class that does too little to justify its       |
|            cost — a shell, a pass-through, an empty child.   |
|  Why bad : Every class charges a fee (name, file, jump).     |
|            Lazy classes pay nothing back and dilute the      |
|            meaning of every real class.                      |
|  Born as : Leftover of refactoring, or speculation that      |
|            never came true.                                  |
|  Test    : "Does it EARN the cost of being separate?"        |
|            Small but protective -> keep. Empty shell -> cut. |
|  Cures   : Inline Class, Collapse Hierarchy, Inline Method,  |
|            Remove Middle Man — or promote it with real work. |
+--------------------------------------------------------------+

✏️ Practice exercise

Below is a tuition-center program with suspicious staff on the payroll. Audit it.

class StudentName {
  constructor(private readonly name: string) {}
  get(): string { return this.name; }
}
 
class AttendanceMarker {
  constructor(private readonly register: AttendanceRegister) {}
  mark(name: StudentName): void {
    this.register.add(name.get());
  }
}
 
class AttendanceRegister {
  private readonly present: string[] = [];
  add(name: string): void {
    if (name.trim().length === 0) throw new Error("Name cannot be empty");
    this.present.push(name);
  }
  count(): number { return this.present.length; }
}
 
class DigitalAttendanceRegister extends AttendanceRegister {
  // planned: cloud sync. status: not planned anymore.
}
 
// usage
const register = new DigitalAttendanceRegister();
const marker = new AttendanceMarker(register);
marker.mark(new StudentName("Riya"));

Your tasks:

  1. There are four classes. For each one, apply the test: "does it earn the cost of being separate?" Write one sentence of judgment per class, and place each one on the quadrant chart from Figure 3.
  2. StudentName wraps a string with no rules. Choose its fate: inline it away, or promote it by moving the empty-name validation from AttendanceRegister into StudentName's constructor. Implement your choice and defend it in one sentence.
  3. AttendanceMarker forwards one call. Apply Inline Class and remove it.
  4. DigitalAttendanceRegister is an empty subclass for a cancelled feature. Apply Collapse Hierarchy.
  5. Write the final, clean usage code. How many classes remain? How many jumps does a reader now make to understand "mark Riya present"? Compare with the four-jump journey in Figure 4.
  6. Bonus: describe one situation in which AttendanceMarker would deserve to exist. (Hint: what if marking attendance also had to notify parents and update fees? Then it would be a real coordinator, not a watchman.)

If your final design has every class doing real, nameable work — you have cured the laziness, and Mrs. Bose would approve your budget.

Frequently asked questions

What is a Lazy Class in simple words?
A Lazy Class is a class that does too little to deserve being a separate class. It may hold one tiny method or just pass calls to someone else. Every class costs attention — a name to learn, a file to open, a jump to follow — and a lazy class gives almost nothing back for that cost.
How do Lazy Classes appear in a codebase?
Usually in two ways. First, as leftovers: a class that once did real work gets hollowed out during refactoring until only a shell remains. Second, as speculation: someone creates a class for a future feature that never arrives, leaving empty structure behind.
Which refactorings fix a Lazy Class?
Inline Class moves the few remaining members into the class that uses them and deletes the shell. Collapse Hierarchy merges a near-empty subclass or superclass with its partner when the inheritance level adds nothing.
Is every small class a Lazy Class?
No. Small is not the same as lazy. A small value object that validates itself, or a type that stops you from mixing a CustomerId with an OrderId, earns its place through safety. The right question is not 'is it small?' but 'does it earn the cost of being separate?'
Should I delete an interface that has only one implementation?
Often yes, but not always. Keep it when it exists for a concrete reason you use today — a genuine test seam, a published extension point, or a dependency boundary. Delete it when it was added only for imagined future flexibility that never came.

Further reading

Related Lessons