Asher Cohen
Back to posts

The Proliferation of Code Anti-Pattern

Understanding and eliminating unnecessary middleman objects that add complexity without value

Introduction

The Proliferation of Code anti-pattern occurs when objects exist solely to invoke another object — acting as unnecessary middlemen. These wrappers add complexity, obscure intent, and make code harder to understand without providing any real value.

What Is Proliferation of Code?

In a modular codebase, objects regularly communicate with each other. The anti-pattern emerges when you create objects whose only purpose is to delegate to another object:

// ❌ Proliferation of Code — unnecessary middleman
class UserServiceProxy {
  constructor() {
    this.realService = new UserService();
  }
  
  getUser(id) {
    return this.realService.getUser(id);
  }
  
  createUser(data) {
    return this.realService.createUser(data);
  }
  
  deleteUser(id) {
    return this.realService.deleteUser(id);
  }
}

// Every method just delegates — no added value!

Why It's Harmful

1. Adds Unnecessary Abstraction

Every layer of indirection is something developers must remember and navigate:

// ❌ Three layers to do one thing
class UserController {
  constructor() {
    this.facade = new UserFacade();
  }
  handleRequest(id) {
    return this.facade.getUser(id);
  }
}

class UserFacade {
  constructor() {
    this.service = new UserService();
  }
  getUser(id) {
    return this.service.getUser(id);
  }
}

class UserService {
  getUser(id) {
    return db.users.findById(id);
  }
}

// ✅ Direct — clear intent
class UserController {
  constructor(userService) {
    this.userService = userService;
  }
  handleRequest(id) {
    return this.userService.getUser(id);
  }
}

2. Obscures Flow and Intent

Middlemen make it harder to trace execution:

// ❌ Where does the work actually happen?
button.onClick → EventHandler.handle() → ActionDispatcher.dispatch() 
→ CommandExecutor.execute() → Service.perform()

// ✅ Clear flow
button.onClick → handleClick() → saveUser()

3. Increases Maintenance Burden

More files to update when interfaces change:

// ❌ Change one method signature → update 3 files
// Interface → Proxy → Facade → Actual implementation

// ✅ Change one method signature → update 1 file
// Interface → Actual implementation

Common Examples

The Empty Wrapper

// ❌ Wraps without adding behavior
class LoggerWrapper {
  constructor() {
    this.logger = new Logger();
  }
  
  log(message) {
    this.logger.log(message);
  }
  
  error(message) {
    this.logger.error(message);
  }
}

// ✅ Use directly or extend meaningfully
const logger = new Logger();
logger.log('message');

// Or extend with real value
class TimestampedLogger extends Logger {
  log(message) {
    super.log(`[${new Date().toISOString()}] ${message}`);
  }
}

The Delegating Component

// ❌ Component that just passes props
function UserCardWrapper({ user }) {
  return <UserCard user={user} />;
}

// ✅ Use UserCard directly
<UserCard user={user} />

The Pass-Through Service

// ❌ Service that only delegates
class OrderService {
  constructor() {
    this.repository = new OrderRepository();
  }
  
  findById(id) {
    return this.repository.findById(id);
  }
  
  save(order) {
    return this.repository.save(order);
  }
}

// ✅ Add real business logic or use repository directly
class OrderService {
  constructor(repository, validator, notifier) {
    this.repository = repository;
    this.validator = validator;
    this.notifier = notifier;
  }
  
  placeOrder(order) {
    this.validator.validate(order);
    const saved = this.repository.save(order);
    this.notifier.notifyOrderPlaced(saved);
    return saved;
  }
}

When Abstraction IS Justified

Not every layer is proliferation. Abstraction is valuable when it:

// ✅ Adds behavior (validation, logging, caching)
class CachedUserService {
  constructor(service) {
    this.service = service;
    this.cache = new Map();
  }
  
  getUser(id) {
    if (this.cache.has(id)) return this.cache.get(id);
    const user = this.service.getUser(id);
    this.cache.set(id, user);
    return user;
  }
}

// ✅ Simplifies a complex interface
class PaymentFacade {
  constructor(gateway, validator, logger) {
    this.gateway = gateway;
    this.validator = validator;
    this.logger = logger;
  }
  
  process(amount, card) {
    this.validator.validate(card);
    const result = this.gateway.charge(amount, card);
    this.logger.log(result);
    return result;
  }
}

// ✅ Provides a different interface (Adapter pattern)
class LegacyToModernAdapter {
  constructor(legacySystem) {
    this.legacy = legacySystem;
  }
  
  async getUser(id) {
    return new Promise((resolve, reject) => {
      this.legacy.fetchUser(id, (err, user) => {
        if (err) reject(err);
        else resolve(user);
      });
    });
  }
}

How to Fix It

1. Remove the Middleman

// Before
class A {
  doWork() { return new B().doWork(); }
}

// After
// Just use B directly where A was used

2. Inline the Delegation

// Before
function getUser(id) {
  return userRepository.getUser(id);
}

// After — call userRepository.getUser(id) directly

3. Merge Responsibilities

// Before: two classes, one delegates to the other
class UserHandler {
  handle(data) { return new UserProcessor().process(data); }
}

// After: merge into one meaningful class
class UserHandler {
  handle(data) {
    const validated = this.validate(data);
    return this.save(validated);
  }
}

Detection Checklist

Ask yourself about each abstraction layer:

  • Does it add behavior (validation, caching, logging)?
  • Does it simplify a complex interface?
  • Does it enable testing through dependency inversion?
  • Does it adapt incompatible interfaces?
  • Would removing it make the code simpler?

If you answered "no" to all questions, you likely have proliferation of code.

Conclusion

The Proliferation of Code anti-pattern is seductive because it feels like good engineering — more layers, more abstraction, more "architecture." But unnecessary middlemen:

  • Obscure intent — Harder to understand what code does
  • Increase maintenance — More files to change
  • Add cognitive load — More things to remember
  • Provide no value — No behavior, no simplification, no adaptation

The fix is simple: remove the middleman. Every abstraction should earn its place by providing real value.

#anti-patterns #clean-code #software-architecture #refactoring #javascript