Middle Man Code Smell: Remove Delegation That Adds No Value

Delegation is useful when one object asks another object to do work. It can separate responsibilities and keep implementation details behind a boundary. But delegation can become ceremony when an object does little more than forward most calls to another object.

That situation is commonly called the Middle Man code smell. The extra layer adds names, files, navigation, and maintenance work without owning a meaningful decision.

The right response is not to remove every forwarding method. Some forwarding methods protect a valuable boundary. This article shows how to tell the difference and how to simplify a Middle Man without exposing details that should remain private.

A Middle Man forwards work without owning a decision

Consider an application service that exposes customer contact data:

class CustomerService:
    constructor(customerRepository):
        this.customerRepository = customerRepository

    emailFor(customerId):
        return this.customerRepository.emailFor(customerId)

    phoneFor(customerId):
        return this.customerRepository.phoneFor(customerId)

    addressFor(customerId):
        return this.customerRepository.addressFor(customerId)

Every operation immediately calls the repository with the same arguments and returns the same result. CustomerService does not apply policy, translate data, coordinate collaborators, enforce permissions, or provide a more stable contract.

A caller has to cross two abstractions to reach one behavior:

checkout -> CustomerService -> CustomerRepository

If the service exists mainly as a forwarding layer, it may be a Middle Man.

The smell is about responsibility, not line count. A method can be one line and still be valuable if that line marks an important boundary. A larger class can still be a Middle Man if most of its behavior merely mirrors another object.

The cost appears during ordinary changes

An unnecessary forwarding layer can seem harmless because each method is simple. Its cost becomes visible when the underlying collaborator changes.

Suppose the repository gains a new operation:

preferredContactFor(customerId)

If callers are required to go through CustomerService, the change may require another forwarding method there:

preferredContactFor(customerId):
    return this.customerRepository.preferredContactFor(customerId)

The service changes even though it contributes no behavior to the feature. Tests may also need another pass-through case. Documentation can acquire another duplicate method description.

This is change amplification: one conceptual change requires edits in layers that add no distinct responsibility.

Navigation suffers too. A developer tracing emailFor() opens the service, sees a forwarding call, then opens the repository. One extra hop is minor. Repeated across a large codebase, such hops make behavior harder to locate.

Direct delegation is the smallest refactoring

If the intermediate object has no useful responsibility, callers can often depend on the real collaborator directly.

Before:

class Checkout:
    constructor(customerService):
        this.customerService = customerService

    sendReceipt(customerId):
        email = this.customerService.emailFor(customerId)
        ...

After:

class Checkout:
    constructor(customerRepository):
        this.customerRepository = customerRepository

    sendReceipt(customerId):
        email = this.customerRepository.emailFor(customerId)
        ...

The forwarding method can then be removed once no callers use it.

This refactoring changes the dependency graph. Checkout now knows CustomerRepository, so that dependency must make sense at the caller’s architectural level. If the repository is an internal detail that checkout code should not know about, direct delegation may be the wrong move.

That constraint is central. Removing a Middle Man should reduce accidental structure, not destroy a deliberate boundary.

A forwarding method can still carry real value

A method does not need complex code to own responsibility. Several kinds of value justify an intermediate boundary.

It presents a stable application concept

Suppose business code calls:

taxes.rateFor(destination)

The implementation currently forwards to one tax provider. Keeping the application-facing operation can be useful even if its body is one line, because callers depend on the concept of a tax rate rather than on a provider API.

The forwarding code is simple, but the boundary owns a dependency decision.

It translates between models

An adapter may appear to delegate while converting application data into a library-specific request and translating the response back.

result = paymentGateway.charge(payment)

If the gateway hides provider request fields, status codes, and error types, it is doing more than forwarding. It prevents provider concepts from spreading through the application.

It enforces policy

A method can add authorization, validation, rate limits, transaction boundaries, audit behavior, or another rule that belongs at that boundary.

For example:

cancelOrder(user, orderId):
    permissions.require(user, "cancel-order")
    return orders.cancel(orderId)

The delegation is visible, but the method owns an access rule. Removing it and calling orders.cancel() directly could bypass that rule unless the rule moves with the refactoring.

It coordinates multiple collaborators

A service that loads an order, applies a policy, persists the result, and emits an event is an orchestrator, not a Middle Man merely because each individual step delegates work.

The useful test is not “does this class call other objects?” Most application code does. Ask what decision or coordination responsibility would disappear if the layer vanished.

Look for duplicated interfaces

One strong signal is an intermediate API that closely copies another API.

Suppose these two types evolve together:

AccountManager.lock(id)
AccountRepository.lock(id)

AccountManager.unlock(id)
AccountRepository.unlock(id)

AccountManager.status(id)
AccountRepository.status(id)

If every manager method has the same name, parameters, return value, and semantics as its repository counterpart, inspect the manager carefully.

A copied interface often means callers are paying for an abstraction that has no independent contract. Every repository addition pressures the manager to add the same method.

Still, similarity alone is not proof. An interface can intentionally isolate callers from a replaceable dependency. In that case, the local interface should represent a boundary the application cares about, and its stability should not depend on copying every operation from the implementation.

A practical sign of a healthy boundary is selectivity: it exposes the capabilities its callers need, not the complete surface of the object behind it.

Remove a Middle Man without making a large rewrite

A safe refactoring can proceed one operation at a time.

Start with a forwarding method that has no extra behavior:

profile.displayName()
    -> userDetails.displayName()

Find its callers and check what dependency they would receive after the change. If direct access is appropriate, move those callers to userDetails.displayName().

Then remove the forwarding method after its callers are gone.

Repeat only where the same reasoning holds. You do not need to eliminate the intermediate type. It may still own other useful behavior.

This incremental approach matters because a class can contain both needless forwarding and legitimate policy:

class AccountService:
    emailFor(id):
        return accounts.emailFor(id)

    closeAccount(user, id):
        permissions.require(user, "close-account")
        account = accounts.find(id)
        account.close()
        accounts.save(account)

emailFor() may be removable while closeAccount() clearly owns coordination and policy. Labeling the whole class a Middle Man would hide that distinction.

Check architectural direction before exposing the collaborator

The easiest local refactoring is not necessarily the right system design.

Imagine a presentation component calls an application service, and that service delegates to a persistence adapter. Removing the service by giving the presentation component direct access to the persistence adapter may reduce one class but couple the presentation layer to storage concerns.

In that case, several alternatives are possible:

  • keep the application boundary even if a particular method is thin;
  • move the useful operation to another application-level object;
  • redesign the boundary around a caller-relevant capability;
  • remove only forwarding methods whose direct dependency remains appropriate.

The choice depends on which dependencies the architecture intends to permit.

A useful rule is: remove delegation only when the resulting dependency is more truthful, not merely shorter.

Common refactoring mistakes

One mistake is counting forwarding methods and applying a threshold. There is no useful universal number. Ten methods that isolate a volatile external API may justify their layer. Two forwarding methods between objects in the same module may be needless.

Another mistake is replacing methods with long call chains:

order.customer().account().preferences().email()

This removes forwarding methods from Order, but it exposes more object structure to callers. The code may become more coupled, not less.

A third mistake is deleting a boundary before checking cross-cutting behavior. Logging, transactions, authorization, metrics, retries, or error translation may be attached to the intermediate layer through framework configuration or decorators rather than visible in the method body. Inspect the actual contract and runtime behavior before removing it.

A fourth mistake is preserving a Middle Man solely because it might become useful later. If the layer has no current responsibility and no concrete boundary to protect, speculative indirection creates present maintenance cost for uncertain future value.

Decide based on owned responsibility

When a forwarding layer feels suspicious, write down what it owns.

A meaningful answer might be “the application contract for payments,” “authorization for account changes,” “translation from provider errors,” or “coordination of the checkout workflow.” Those are responsibilities that can justify a thin implementation.

If the answer is only “it calls the next object,” inspect whether callers can use that object directly without crossing an architectural boundary or depending on unstable details.

The goal of removing a Middle Man is not fewer classes at any cost. It is a dependency structure in which each layer earns its place by owning a decision, policy, translation, or coordination responsibility. When a layer adds none of those, removing it can make both navigation and future changes more direct.