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.
🛗 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.
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:
🔍 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).
| Sign | Question to ask | If the answer is... |
|---|---|---|
| One tiny method | Could this be a method on its caller? | Yes → lazy |
| Wrapper around one field | Does the wrapper validate, format, or protect anything? | No → lazy |
| Subclass adds nothing | Does it override or extend any behavior? | No → lazy |
| Interface with one implementation | Is there a real test seam or published boundary today? | No → lazy |
| Class shrunk by refactoring | Did its responsibilities move elsewhere? | Yes → leftover shell |
| "Manager"/"Helper" with 10 lines | Does 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?
⚠️ 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?":
And the tax compounds as shells accumulate:
The life story of a lazy class explains why nobody notices the problem while it forms:
💻 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:
LiftButtonwraps 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.LiftWatchmanreceives 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.AutomaticLiftis 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:
If you audited a typical mature codebase for this smell, the suspicious classes usually fall into these buckets:
🧹 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.
🟦 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
IFooServicefor everyFooServiceby 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 Userwith an empty body, waiting three years for premium features. This is Speculative Generality leaving lazy fossils behind. - Wrapper-of-a-wrapper layers.
OrderFacadecallsOrderManagercallsOrderServicecalls 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 class | Keep or remove? | Why |
|---|---|---|
| Value object that validates on construction | Keep | It guards an invariant; safety is real work |
Distinct ID types (CustomerId vs OrderId) | Keep | The type system now prevents mix-up bugs |
| Interface with a real test seam used today | Keep | Faking a hard dependency is a concrete benefit |
| Published extension point with external users | Keep | Outsiders depend on it; removing breaks them |
| Class that is small today but actively growing | Keep, watch | It is a seedling, not a fossil |
| Hollowed-out leftover of old refactorings | Remove | A shell teaches nothing and costs a jump |
| Empty subclass for an imagined future | Remove | Add it back when the future actually arrives |
| Pure pass-through wrapper | Remove | Middle 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
| Situation | Curing refactoring | Result |
|---|---|---|
| Class with few members, one main user | Inline Class | Members move into the user; shell deleted |
| Near-empty subclass or superclass | Collapse Hierarchy | Two hierarchy levels merge into one |
| Tiny pass-through method left behind | Inline Method | The indirection disappears |
| Class only delegates to another | Remove Middle Man | Callers talk to the real worker directly |
| Wrapper that should grow instead | Add 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:
- 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.
StudentNamewraps a string with no rules. Choose its fate: inline it away, or promote it by moving the empty-name validation fromAttendanceRegisterintoStudentName's constructor. Implement your choice and defend it in one sentence.AttendanceMarkerforwards one call. Apply Inline Class and remove it.DigitalAttendanceRegisteris an empty subclass for a cancelled feature. Apply Collapse Hierarchy.- 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.
- Bonus: describe one situation in which
AttendanceMarkerwould 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
Speculative Generality: Plumbing for a Swimming Pool You May Never Build
Learn the Speculative Generality smell with a house-building story. Understand YAGNI, why guessing future needs backfires, and how to collapse unused abstractions.
Data Class: The Register With No Rules — Anyone Can Scribble Anything
Learn the Data Class smell with a society register story. See why data without behavior breaks encapsulation, and when DTOs and records are perfectly fine.
Middle Man: The Helper Who Only Forwards Your Message to the Principal
Learn the Middle Man code smell with a story of a school helper who only carries messages without adding anything. When a class merely forwards every call, remove it — but learn why Proxy, Facade, and Adapter are middle men ON PURPOSE.
Dead Code: Old Furniture Blocking the Storeroom 'Just in Case'
Learn the Dead Code smell with a storeroom full of unused furniture. See why unreachable code costs real money, with the Knight Capital story and easy fixes.