Remove Middle Man: When the Peon Only Forwards, Meet the Principal Directly
Learn the Remove Middle Man refactoring with a story about a school peon who forwards every question to the principal without adding anything. When a class only forwards calls to its delegate, delete the forwarding and let clients talk to the delegate directly. Step-by-step TypeScript and C# walkthrough.
🪑 Raghu, the peon who forwards every single question
Outside the principal's office of a school in Nagpur sits Raghu, the peon. His chair is right next to the principal's door. The principal, Mrs. Iyer, is a kind lady who keeps her door open most of the day.
A student named Kabir comes with a question: "When does the fee counter close?" Raghu does not know. He gets up, opens the door, asks Mrs. Iyer the exact same words, listens, comes back, and repeats the answer to Kabir: "Four thirty." Kabir asks another: "Is tomorrow a holiday?" Again Raghu opens the door, carries the same question in, carries the same answer out. Word for word. He adds nothing. He checks nothing. He shortens nothing. He does not even batch Kabir's two questions into one trip.
One afternoon Kabir stands there, watching Raghu walk the same three steps for the fourth time, and a thought strikes him: the door is right there. Mrs. Iyer is free and friendly. Why am I talking through a human photocopy machine? He politely steps past the chair, knocks, asks his question directly, and gets the answer in half the time. Mrs. Iyer smiles — she never asked for a gatekeeper in the first place.
Now be fair to peons in general — many do real work. A good office assistant filters visitors, batches questions, protects the principal's time, answers common things himself, and turns away salesmen at exam time. That middle man earns his chair. The problem is only the one who does pure forwarding: every question in, the same question out, nothing added in between.
Code grows this exact character. A class starts life doing real work. Slowly its useful parts move elsewhere, or it accumulates forwarding methods one by one — often from over-enthusiastic use of Hide Delegate. One day you read it and find ten methods, and nine of them are one-liners that just call the same inner object. The class has become a toll booth on a free road.
The cure is the Remove Middle Man refactoring: open the door, let clients talk to the inner object directly, and delete the parrot methods.
🔍 What is Remove Middle Man?
Remove Middle Man is a refactoring from Fowler's catalog. The recipe: when a class does little except forward calls to a delegate, give clients direct access to the delegate, redirect the calls, and delete the forwarding methods.
Here is the classic shape. A Customer forwards everything to its Account:
class Account {
balance(): number { /* real work */ return 5230; }
interestRate(): number { return 0.04; }
overdraftLimit(): number { return 10000; }
monthlyStatement(): string { return "..."; }
}
class Customer {
constructor(private account: Account) {}
balance(): number { return this.account.balance(); }
interestRate(): number { return this.account.interestRate(); }
overdraftLimit(): number { return this.account.overdraftLimit(); }
monthlyStatement(): string { return this.account.monthlyStatement(); }
}Four methods, four pure forwards, zero added value. Every time Account learns a new trick, someone must teach Customer the same trick — forever. After Remove Middle Man:
class Customer {
constructor(public readonly account: Account) {}
// only methods with REAL customer logic remain here
}
// Clients reach the account directly:
const limit = customer.account.overdraftLimit();Customer stops pretending to be a bank account. Clients that need account behavior get the real account, with its full interface, and Customer keeps only what is truly about customers.
How bad is it, in numbers? Open the class and sort its methods into two buckets:
Quick smell test: open the class and count. How many methods contain the words return this.something.sameName() and nothing else? If that number is more than half the class, you are looking at a Middle Man. One or two meaningful forwards are fine — wholesale parroting is not.
College corner: Why do middle men keep growing once they exist? Because of an interface synchronisation cost. A pure-forwarding class C wrapping delegate D must mirror every operation of D that any client wants — so the size of C's interface is lower-bounded by client demand on D. Each new D feature triggers a matching C edit: that is Shotgun Surgery built into the design. Delegation also has a small runtime cost (an extra call frame, usually inlined away by JITs) — but the cost that matters is cognitive: every reader of customer.balance() must discover that the real logic lives a level deeper. Indirection is only worth paying for when it abstracts something; forwarding that renames nothing and decides nothing is indirection without abstraction, which is the textbook definition of accidental complexity.
🚦 When do we need it?
Look for these signals:
- The class is mostly one-line forwards. This is the textbook Middle Man smell — and Remove Middle Man is its direct cure.
- A maintenance treadmill. Every new method on the delegate forces a matching method on the server. You are paying rent for encapsulation nobody uses.
- Over-applied Hide Delegate. Someone (maybe past you!) wrapped every hop to fight Message Chains, and the wrapping went too far. The dial swung past the healthy middle.
- The forwarding hides nothing real. The relationship between server and delegate is public knowledge, stable, and unsurprising — so the "hiding" protects no secret.
And the signals to leave it alone:
- The middle man exists on purpose. Proxy, Decorator, and Facade patterns are deliberate middle men with jobs: lazy loading, added behavior, simplified interfaces. Removing them removes a feature.
- Only a few methods forward, and they hide a relationship clients should not depend on. Keep those.
- The delegate's interface is unstable. Exposing it couples every client to its changes.
| Situation | Verdict |
|---|---|
| Nine of ten methods are one-line forwards | Remove the middle man |
| Every sprint adds a matching parrot method | Remove the middle man |
| The wrapper validates, combines, or caches | Keep it — it earns its chair |
| The wrapper is a Proxy, Decorator, or Facade | Keep it — it is a feature |
| The delegate's API changes every month | Keep hiding it for now |
The treadmill in signal 2 is easy to see on a chart. Watch what happens to a wrapped class across three releases of its delegate:
👀 Before and after at a glance
// ---------- BEFORE: Customer is a toll booth ----------
class Customer {
constructor(private account: Account) {}
balance(): number { return this.account.balance(); }
interestRate(): number { return this.account.interestRate(); }
overdraftLimit(): number { return this.account.overdraftLimit(); }
monthlyStatement(): string { return this.account.monthlyStatement(); }
greetingName(): string { return `Shri ${this.name}`; } // the ONE real method
private name = "Mehta";
}
// Client:
const limit = customer.overdraftLimit();// ---------- AFTER: door open, parrots deleted ----------
class Customer {
constructor(public readonly account: Account) {}
greetingName(): string { return `Shri ${this.name}`; } // real value stays
private name = "Mehta";
}
// Client:
const limit = customer.account.overdraftLimit();One extra dot appeared at the call site — customer.account. — and in exchange, four maintenance-only methods vanished and will never need siblings.
🪜 Step-by-step, the safe way
Step 1 — Confirm the diagnosis. List the class's methods in two columns: "adds value" and "pure forward." Proceed only if pure forwards dominate, and only if the class is not a deliberate Proxy/Decorator.
Step 2 — Expose the delegate. Add an accessor (or widen visibility) so clients can reach the delegate. Change nothing else:
class Customer {
constructor(private _account: Account) {}
get account(): Account { // new door, nothing removed yet
return this._account;
}
balance(): number { return this._account.balance(); } // still here
// ...other forwards still here
}Compile, run tests. Green — because nothing moved yet.
Step 3 — Redirect callers of ONE forwarding method. Pick balance(). Find its callers. Change each customer.balance() to customer.account.balance(), one call site at a time, testing between changes.
Step 4 — Delete the empty forward. When balance() has zero callers, delete it. Compile and test. The compiler confirms nobody misses it.
Step 5 — Repeat for each forwarding method. interestRate(), then overdraftLimit(), then monthlyStatement(). Each loop is the same: redirect, verify, delete.
Step 6 — Reassess the survivor. Look at what remains of Customer. If it still has real responsibilities, you are done. If it is now nearly empty — a Lazy Class — consider going one step further with Inline Class and folding it away entirely.
The class itself moves through clear states during this surgery, and each state is a safe place to pause:
Do not delete all forwarding methods first and fix callers later. That creates one giant broken build where every error fights for your attention. The safe rhythm is per-method: redirect its callers, run the tests, then delete it. Each green test run is a save point you can return to.
🛵 A bigger real-life example
A food-delivery app has a DeliveryAgent class. Once upon a time it did routing math itself. Then a Route class was extracted — good! — but DeliveryAgent kept forwarding everything, so the extraction never finished:
class Route {
constructor(private stops: string[]) {}
totalDistanceKm(): number { return this.stops.length * 1.8; }
estimatedMinutes(): number { return this.totalDistanceKm() * 4; }
nextStop(): string { return this.stops[0]; }
remainingStops(): number { return this.stops.length; }
}
class DeliveryAgent {
constructor(
public name: string,
private route: Route,
private rating: number
) {}
// --- pure forwards: the middle-man layer ---
totalDistanceKm(): number { return this.route.totalDistanceKm(); }
estimatedMinutes(): number { return this.route.estimatedMinutes(); }
nextStop(): string { return this.route.nextStop(); }
remainingStops(): number { return this.route.remainingStops(); }
// --- the only real agent logic ---
isTopRated(): boolean { return this.rating >= 4.5; }
}
// Clients:
console.log(`${agent.name} reaches in ${agent.estimatedMinutes()} min`);
console.log(`Next stop: ${agent.nextStop()}`);Four forwards versus one real method. Every sprint, Route gains features (tollCost(), trafficDelay()...), and every sprint someone dutifully adds matching parrots to DeliveryAgent. Time to remove the middle man:
class DeliveryAgent {
constructor(
public name: string,
public readonly route: Route, // the door is open
private rating: number
) {}
isTopRated(): boolean { return this.rating >= 4.5; }
}
// Clients talk to the object that actually knows:
console.log(`${agent.name} reaches in ${agent.route.estimatedMinutes()} min`);
console.log(`Next stop: ${agent.route.nextStop()}`);When Route gains tollCost() next sprint, clients call agent.route.tollCost() on day one. No parrot required. And DeliveryAgent finally reads honestly: it is a person with a name, a rating, and an assigned route — not a fake routing engine.
One nuance worth keeping: suppose the app shows "agent is nearby" on the customer screen, and nearby means something agent-specific (say, fewer than 2 remaining stops AND top rated). That combination logic belongs ON the agent:
class DeliveryAgent {
// ...
isNearby(): boolean {
return this.route.remainingStops() < 2 && this.isTopRated();
}
}This is not a parrot — it combines delegate data with its own data. Remove Middle Man deletes echoes, never brains.
💻 The same refactoring in C#
The C# version reads almost identically. Before:
public class Customer
{
private readonly Account _account;
public Customer(Account account) => _account = account;
public decimal Balance => _account.Balance;
public decimal InterestRate => _account.InterestRate;
public decimal OverdraftLimit => _account.OverdraftLimit;
public string MonthlyStatement() => _account.MonthlyStatement();
}
// Client:
var limit = customer.OverdraftLimit;After — expose the delegate as a read-only property and delete the echoes:
public class Customer
{
public Account Account { get; }
public Customer(Account account) => Account = account;
// only genuinely customer-flavored members remain
}
// Client:
var limit = customer.Account.OverdraftLimit;A practical C# tip: delete one forwarding member at a time and build. Every compile error is a caller waiting to be redirected to customer.Account.X. When the build is green, that member is fully migrated — move to the next. With readonly/get-only exposure, you open the door without letting anyone swap the account out from under the customer.
🐍 And once in Python
In Python the middle man often hides as a stack of tiny def one-liners. The cleanup is the same: expose the delegate, delete the echoes.
class Customer:
def __init__(self, account):
self.account = account # the open door (was _account)
def greeting_name(self): # real customer logic stays
return f"Shri {self._name}"
# Clients go direct:
limit = customer.account.overdraft_limit()A Python-specific trap: some middle men hide behind __getattr__, forwarding everything dynamically. That is even worse than explicit parrots — readers cannot see what the class offers at all, and IDEs cannot autocomplete it. If you find one of those wrapping a stable delegate, removing it usually improves both clarity and tooling in one stroke.
📈 Should this class live or go? Read the dials
Two pictures help you decide with a cool head instead of a mood. First, place the suspect class on the decision map:
Second, run the method audit (Figure 4 style) on your own class. If the forwarding slice is over half, schedule the cleanup. If it is a sliver, leave the class in peace — one or two meaningful forwards are healthy hiding, not a disease.
⚖️ The seesaw: Remove Middle Man vs Hide Delegate
This refactoring is the exact mirror of Hide Delegate. Together they form one dial with two failure modes at the ends:
- Clients full of chains (
a.b.c.deverywhere) → they know too much structure → turn the dial with Hide Delegate. - Classes full of pure forwarding → toll booths with no toll → turn the dial back with Remove Middle Man.
How much hiding is right? Fowler's honest answer: it changes over time. A wrapper that protected a volatile relationship five years ago may guard a relationship that has not moved since — now it is just toll. Re-judge periodically, and be comfortable turning the dial in either direction. Refactoring is not a one-way street; it is steering.
College corner: The "never touch" list deserves a sharper statement. Proxy, Decorator, and Facade are middle men by contract: a Proxy controls access (lazy loading, permissions, remoting), a Decorator layers behavior while preserving the interface, a Facade compresses a subsystem into a simple front. Their forwarding is the mechanism, not the smell — the added value lives in when and how they forward, not in changing the message. The distinguishing test in a code review: ask what breaks if the layer disappears. For a true middle man the answer is "nothing, callers just dot one level deeper." For a Proxy the answer might be "we load 10,000 images eagerly at startup." If removing the layer removes a behavior, it was never a middle man.
✅ Benefits and risks
| Benefit | Why it matters |
|---|---|
| Dead-weight methods disappear | Less code to read, test, and maintain |
| The treadmill stops | New delegate features need no matching server method |
| Clients get the full delegate API | No intermediary filtering or lagging behind |
| The server's true job becomes visible | Sometimes revealing it is tiny — inviting further cleanup |
| Risk | How to handle it |
|---|---|
| Message chains return at call sites | Accept consciously for stable relationships; re-hide if structure starts changing |
| The delegate becomes public API | Its changes now hit clients directly — expose only stable delegates |
| Over-removal deletes meaningful wrappers | Keep forwards that validate, combine, or genuinely hide volatile structure |
| Deliberate middle men get destroyed | Never remove a Proxy, Decorator, or Facade doing its designed job |
🧪 Which smells does it cure?
| Smell | How Remove Middle Man helps |
|---|---|
| Middle Man | The direct cure — deletes the pure-forwarding layer |
| Lazy Class | Often the follow-up: a stripped server with nothing left gets inlined away |
| Speculative Generality | Removes "just in case" indirection nobody ever needed |
| Message Chains | ⚠️ Does NOT cure it — over-applying Remove Middle Man creates it |
🛠️ IDE support
This refactoring enjoys unusually good automation:
- IntelliJ IDEA / JetBrains family: ships a dedicated Remove Middleman refactoring. Place the caret on the delegate field, invoke it, and the IDE replaces all calls to the delegating methods with direct calls on the delegate, optionally deleting the now-unused forwards in one shot.
- Rider / ReSharper (C#): no single-named command, but Inline Method on each forwarding member does the same job — it rewrites every caller to the underlying
_account.Xcall and removes the member. Combine with Find Usages to do it method by method. - Visual Studio: use the built-in Inline Method (Quick Actions, Ctrl+.) on each forwarder, or delete-and-follow-compile-errors with Find All References as your map.
- VS Code (TypeScript): no automated inline-method across files yet; rely on Find All References, hand-edit call sites, and let
tscconfirm each deleted forward had no surviving callers.
Whatever the tool, keep the per-method rhythm: inline or redirect one forwarder, run tests, delete, repeat.
📦 Quick revision box
+----------------------------------------------------------------+
| REMOVE MIDDLE MAN — CHEAT SHEET |
+----------------------------------------------------------------+
| Smell to spot : class where most methods just forward |
| Move : expose the delegate via an accessor |
| Then : redirect callers to delegate, method by method |
| Finally : delete each forwarding method when unused |
| Keep : wrappers that add checks, meaning, or combos |
| Never touch : deliberate Proxy / Decorator / Facade |
| Danger : over-removal -> Message Chains come back |
| Opposite move : Hide Delegate (same dial, other end) |
| Follow-up : nearly-empty class? consider Inline Class |
+----------------------------------------------------------------+✏️ Practice exercise
Here is a hostel management class that has drifted into toll-booth life. Refactor it.
class MessMenu {
breakfast(): string { return "Poha and chai"; }
lunch(): string { return "Dal, rice, sabzi"; }
dinner(): string { return "Roti, paneer"; }
isSpecialDay(): boolean { return new Date().getDay() === 0; }
}
class Warden {
constructor(private menu: MessMenu, private name: string) {}
breakfast(): string { return this.menu.breakfast(); }
lunch(): string { return this.menu.lunch(); }
dinner(): string { return this.menu.dinner(); }
isSpecialDay(): boolean { return this.menu.isSpecialDay(); }
noticeBoardMessage(): string {
return `Warden ${this.name}: lights off at 10 pm.`;
}
}
// Clients:
console.log(warden.breakfast());
console.log(warden.lunch());
console.log(warden.isSpecialDay() ? "Sunday special!" : "Regular day");Your tasks:
- Classify each
Wardenmethod: pure forward or real value? Draw your own pie like Figure 4 — what fraction forwards? - Expose
menusafely (read-only), then migrate ONE forwarding method completely — redirect its callers, test, delete it. Repeat for the rest, pausing at each state from Figure 7. - After cleanup,
Wardenhas one real method. IsWardenstill worth keeping as a class, or should it inline away? Write your one-line verdict. - Twist: the hostel now wants
sundayFeast()— which returns the dinner menu plus "kheer" only whenisSpecialDay()is true. Where does this method belong: onMessMenu, or onWarden? Why is it NOT a middle-man method even though it calls the menu? - Pattern check: suppose the hostel adds a
CachedMessMenuthat wrapsMessMenuand remembers today's answers so the cook is asked only once. It forwards every method. Should you remove it? Use the what-breaks-if-it-disappears test from the College corner.
If you answered task 4 with "because it combines and decides instead of echoing," and task 5 with "no — caching is a job, this is a Proxy," you have mastered the difference between a parrot and a brain — which is the whole art of this refactoring. Raghu's chair, by the way, did not stay empty: Mrs. Iyer gave him a notice board, a question diary, and the authority to answer the common questions himself. He became a Facade. Nobody wants to remove him now.
Frequently asked questions
- What is the Remove Middle Man refactoring?
- Remove Middle Man deletes pass-through methods from a class that has become pure forwarding. You add an accessor that exposes the delegate, redirect every caller to use the delegate directly, and then delete the dead forwarding methods. The class shrinks back to only the work it genuinely does.
- How do I know a class is a Middle Man worth removing?
- Count its methods. If half or more are one-liners that just call the same delegate and return the result, and every new delegate feature forces yet another forwarding method, the class is a toll booth. Fowler's rough guide: when most of a class's methods are delegating, remove the middle man.
- Is Remove Middle Man the opposite of Hide Delegate?
- Yes, exactly. Hide Delegate adds forwarding methods so clients stop walking chains. Remove Middle Man deletes forwarding methods when they have piled up into noise. They turn the same dial in opposite directions, and healthy code sits between the two extremes.
- Will removing a middle man bring back message chains?
- It can — that is the trade you are consciously making. Exposing the delegate means clients write server.delegate.method() again. That is acceptable when the relationship is stable and clients genuinely need the delegate's rich interface. If the chains start leaking structure that changes often, turn the dial back with Hide Delegate.
- Should I always remove every forwarding method?
- No. Partial removal is often best: expose the delegate for the heavily-used calls, but keep the few wrappers that add real meaning, checks, or convenience. Also never remove middle men that exist on purpose — Proxy and Decorator patterns are deliberate middle men with a job.
Further reading
Related Lessons
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.
Message Chains: Asking a Friend, Who Asks a Cousin, Who Asks an Uncle
Learn the Message Chains code smell with a story about finding out if bread is available — through four people. When code reads a.getB().getC().getD(), the caller is coupled to a whole path. Learn the Law of Demeter and cure chains with Hide Delegate.
Hide Delegate: Ask the Monitor, Let the Monitor Do the Running
Learn the Hide Delegate refactoring with a story about a class monitor who finds your homework for you. Stop writing chains like employee.department.manager — give the first object a simple method and hide the journey inside it. Step-by-step TypeScript and C# examples included.
Inline Class: Merge a Class That Does Too Little
Learn the Inline Class refactoring through a school committee story. Merge a class that does too little back into its user and remove useless indirection.