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