Reference

Anti-Patterns

The wrong approach for each pattern — how to recognise and fix them.

01
Introduction

What Are Anti-Patterns?

An anti-pattern is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive. Unlike a simple mistake, an anti-pattern looks reasonable at first glance — it often works in a prototype or under minimal load — but as the system grows, it creates compounding damage: tight coupling, untestable code, explosive class counts, or hidden shared state.

Analogy: An anti-pattern is like duct-taping a leaky pipe. It stops the drip today, but six months later you have water damage in the walls. A design pattern is calling the plumber — it costs more upfront but the problem is actually solved.

Why this page matters:

  • You can’t appreciate a pattern until you feel the pain of the anti-pattern it replaces
  • In interviews, explaining what goes wrong without the pattern is as important as explaining the pattern itself
  • In code reviews, naming the anti-pattern gives your team a shared vocabulary for identifying structural debt
How to read this page: Each anti-pattern below follows the same structure:
  • What it looks like — a code snippet or structural description
  • Why it breaks — the specific damage it causes as the system grows
  • The fix — which GoF pattern solves it, with a link to the full pattern page
02
Quick Reference

Anti-Pattern Catalog

Anti-Pattern What It Looks Like Pattern That Fixes It
Singleton Abuse Using Singleton as a global variable bag Singleton (used correctly) or DI
Subclass Explosion 12 subclasses for every combination of features Decorator
Giant Conditional if/else or switch on type in every method Strategy / State
Hardcoded Dependencies new ConcreteClass() scattered everywhere Factory Method / DI
Blob / God Object One class that knows and does everything Mediator, Facade
Parallel Inheritance Every subclass of A requires a matching subclass of B Bridge
Poltergeist A class that only passes messages, has no real state Remove entirely or Facade
03
Anti-Pattern #1

Singleton Abuse

The smell: Using Singleton as a dumping ground for global state — config values, user session, database connection, feature flags, and logging all crammed into one AppContext.getInstance().
✗ What it looks like
class AppContext { private static AppContext instance; private String dbUrl; private User currentUser; private Map<String, Object> featureFlags; private Logger logger; private Connection dbConnection; // ... 200 more fields public static AppContext getInstance() { if (instance == null) instance = new AppContext(); return instance; } } // Used everywhere โ€” impossible to test class OrderService { public void placeOrder() { AppContext.getInstance().getLogger().info("placing order"); AppContext.getInstance().getDbConnection().execute(...); AppContext.getInstance().getCurrentUser()... } }

Why it breaks:

  • Hidden dependenciesOrderService depends on logging, DB, and user session, but its constructor signature shows none of that
  • Untestable — you cannot mock AppContext.getInstance() without reflection hacks or PowerMock
  • Thread-safety nightmare — one mutable global accessed by every thread
  • God Object — the Singleton becomes the dumping ground for everything
The fix:
  • Use Dependency Injection — pass each service (Logger, DataSource, UserContext) explicitly via constructor
  • If you truly need a single instance, let the DI container manage the lifecycle (@Singleton scope) rather than hardcoding it
  • Reserve Singleton for things that are genuinely one-per-JVM: a thread pool, a system clock wrapper

โ†’ Read the Singleton pattern page (covers correct usage and thread-safety)

04
Anti-Pattern #2

Subclass Explosion

The smell: Every combination of features requires its own subclass. Adding one feature doubles the number of classes.
✗ What it looks like — a coffee shop with combinatorial subclasses
// Base: Espresso // Add milk? โ†’ EspressoWithMilk // Add sugar? โ†’ EspressoWithSugar // Both? โ†’ EspressoWithMilkAndSugar // Add whip? โ†’ EspressoWithMilkAndSugarAndWhip // Different base? โ†’ Latte, LatteWithMilk, LatteWithSugar... // // 3 bases ร— 4 optional toppings = 48 subclasses ๐Ÿ’€ class EspressoWithMilkAndSugar extends Espresso { ... } class EspressoWithMilkAndWhip extends Espresso { ... } class LatteWithSugarAndWhip extends Latte { ... } // ... 45 more classes

Why it breaks:

  • Combinatorial explosion — N features × M bases = N×M classes (or worse, 2N if features are optional)
  • Impossible to maintain — a bug in cost calculation must be fixed in dozens of classes
  • Rigid — adding a new feature (e.g. “oat milk”) requires touching every existing combination
Subclass Explosion vs. Decorator โ€” class count comparison
WITHOUT (Subclass Explosion) 48 classes 3 bases × 2&sup4; topping combos WITH (Decorator) 7 classes 3 bases + 4 decorator wrappers Same functionality, 85% fewer classes โ€” and adding a new topping = 1 new class
The fix: Decorator pattern
  • Each “feature” becomes a wrapper that implements the same interface
  • Stack wrappers at runtime: new Whip(new Sugar(new Milk(new Espresso())))
  • Adding a new feature = one new wrapper class, zero changes to existing code
05
Anti-Pattern #3

Giant Conditional

The smell: A growing if/else or switch on type that appears in multiple methods across the class. Every new type requires editing every method.
✗ What it looks like — payment processing with type-switching
class PaymentProcessor { public void process(String type, double amount) { if (type.equals("credit_card")) { validateCard(); chargeCard(amount); sendReceipt(); } else if (type.equals("paypal")) { redirectToPayPal(); chargePayPal(amount); sendReceipt(); } else if (type.equals("crypto")) { generateWallet(); chargeCrypto(amount); confirmOnChain(); } else if (type.equals("bank_transfer")) { // added last sprint... } // Apple Pay? Google Pay? Buy Now Pay Later? More branches... } public void refund(String type, double amount) { // Same if/else chain duplicated here } public String getReceipt(String type) { // And here... } }

Why it breaks:

  • Open/Closed violation — every new payment type edits multiple methods in an existing class
  • Duplicated switching logic — the same conditional appears in process(), refund(), getReceipt()
  • Single Responsibility violation — one class knows the internals of every payment type
  • Untestable in isolation — testing crypto logic requires instantiating the entire processor
The fix:
  • Strategy — extract each branch into a PaymentStrategy implementation; the processor delegates to whichever strategy is injected
  • State — if the conditional depends on the object’s lifecycle phase (Pending, Paid, Refunded), model each phase as a State class
  • Adding a new payment method = one new class, zero edits to existing code
✔ After โ€” Strategy replaces the conditional
interface PaymentStrategy { void process(double amount); void refund(double amount); String getReceipt(); } class CreditCardPayment implements PaymentStrategy { /* ... */ } class PayPalPayment implements PaymentStrategy { /* ... */ } class CryptoPayment implements PaymentStrategy { /* ... */ } class PaymentProcessor { private PaymentStrategy strategy; public PaymentProcessor(PaymentStrategy strategy) { this.strategy = strategy; } public void process(double amount) { strategy.process(amount); } }
06
Anti-Pattern #4

Hardcoded Dependencies

The smell: new ConcreteClass() scattered throughout the codebase. The class that uses a service also creates it — coupling usage to a specific implementation.
✗ What it looks like
class ReportGenerator { public void generate() { MySqlDatabase db = new MySqlDatabase(); // hardcoded PdfExporter exporter = new PdfExporter(); // hardcoded SmtpMailer mailer = new SmtpMailer(); // hardcoded var data = db.query("SELECT ..."); var pdf = exporter.export(data); mailer.send(pdf); } }

Why it breaks:

  • Cannot swap implementations — switching from MySQL to PostgreSQL means editing ReportGenerator
  • Cannot test — unit tests must spin up a real database and SMTP server
  • Scattered new calls — if PdfExporter’s constructor changes, you hunt down every instantiation site
  • Violates Dependency Inversion — high-level module depends on low-level concrete classes
The fix:
  • Factory Method — move new into a factory method that subclasses override; the generator works against interfaces only
  • Dependency Injection — pass Database, Exporter, Mailer interfaces through the constructor
  • Abstract Factory — when you need families of related dependencies that must match (e.g. all AWS or all GCP services)
✔ After โ€” dependencies injected via constructor
class ReportGenerator { private final Database db; // interface private final Exporter exporter; // interface private final Mailer mailer; // interface public ReportGenerator(Database db, Exporter exp, Mailer m) { this.db = db; this.exporter = exp; this.mailer = m; } public void generate() { var data = db.query("SELECT ..."); // works with any DB var doc = exporter.export(data); // PDF, CSV, whatever mailer.send(doc); // SMTP, SES, mock } }
07
Anti-Pattern #5

Blob / God Object

The smell: One class that knows everything, does everything, and is imported by everything. It has 50+ fields, 100+ methods, and a 3000-line source file. Every change touches it.
✗ What it looks like
class ApplicationManager { // User management public void registerUser(...) { ... } public void loginUser(...) { ... } public void resetPassword(...) { ... } // Order processing public void placeOrder(...) { ... } public void cancelOrder(...) { ... } public void refundOrder(...) { ... } // Inventory public void updateStock(...) { ... } public void reorderProduct(...) { ... } // Notifications public void sendEmail(...) { ... } public void sendSms(...) { ... } // ... 80 more methods across 6 unrelated domains }

Why it breaks:

  • Merge conflicts — with 5 developers working in the same file, every PR conflicts
  • Impossible to reason about — a change to order logic might accidentally break notifications
  • Cannot reuse — you cannot use the notification logic without importing the entire 3000-line class
  • Cannot test — testing placeOrder() requires mocking user management, inventory, and email
The fix:
  • Facade — if external callers need a simple interface, keep a thin Facade that delegates to focused services (UserService, OrderService, NotificationService)
  • Mediator — if the services need to communicate, introduce a Mediator rather than letting them reference each other directly
  • Extract classes — the core fix is decomposition: each cohesive group of methods becomes its own service with a single responsibility
08
Anti-Pattern #6

Parallel Inheritance

The smell: Every time you add a subclass of Shape, you must also add a matching subclass of Renderer. Two hierarchies grow in lockstep — forget one and the system breaks.
✗ What it looks like
// Shape hierarchy class CircleSvgRenderer extends SvgRenderer { ... } class CircleCanvasRenderer extends CanvasRenderer { ... } class CirclePdfRenderer extends PdfRenderer { ... } class SquareSvgRenderer extends SvgRenderer { ... } class SquareCanvasRenderer extends CanvasRenderer { ... } class SquarePdfRenderer extends PdfRenderer { ... } // Add Triangle? โ†’ 3 new renderer classes required // Add WebGL renderer? โ†’ 1 new class per shape // M shapes ร— N renderers = Mร—N classes ๐Ÿ’€

Why it breaks:

  • M × N class explosion — every new dimension multiplies the class count
  • Forced lockstep — adding a shape without adding all its renderers causes runtime errors
  • Scattered changes — modifying rendering logic means touching N files (one per shape)
The fix: Bridge pattern
  • Split into two independent hierarchies connected by composition: Shape holds a reference to Renderer
  • Circle calls renderer.drawCircle(x, y, radius) — any renderer works with any shape
  • M shapes + N renderers = M + N classes (not M × N)
✔ After โ€” Bridge separates the two hierarchies
interface Renderer { void renderCircle(int x, int y, int r); void renderSquare(int x, int y, int side); } abstract class Shape { protected Renderer renderer; // bridge abstract void draw(); } class Circle extends Shape { void draw() { renderer.renderCircle(x, y, radius); } } // 3 shapes + 3 renderers = 6 classes (not 9)
09
Anti-Pattern #7

Poltergeist

The smell: A class with no state, no meaningful logic, and only one job: forwarding calls to another class. It exists only to “manage” or “control” something — but adds zero value.
✗ What it looks like
class OrderController { private OrderService service = new OrderService(); public void createOrder(Order o) { service.createOrder(o); // just forwards } public void cancelOrder(int id) { service.cancelOrder(id); // just forwards } public Order getOrder(int id) { return service.getOrder(id); // just forwards } // No validation, no transformation, no added logic }

Why it breaks:

  • Unnecessary indirection — callers could use OrderService directly with no loss
  • Maintenance burden — every new method on OrderService requires a matching passthrough method
  • Obscures architecture — readers think the controller does something meaningful when it doesn’t
  • Violates YAGNI — the class was added “in case we need it later” but never grew into anything useful
The fix:
  • Delete it — if the class adds no logic, no validation, and no transformation, remove it and let callers use the real service directly
  • Facade — if this class should exist because it simplifies a complex subsystem, give it real orchestration logic (transaction management, input validation, error handling) instead of raw forwarding
  • Rule of thumb: If a class has only forwarding methods and no state, it’s a Poltergeist. If it coordinates multiple services into a simpler API, it’s a legitimate Facade.
10
Summary

Anti-Pattern → Pattern Fix Map

Each anti-pattern and the GoF pattern that resolves it
ANTI-PATTERN (the problem) Singleton Abuse Subclass Explosion Giant Conditional Hardcoded Dependencies God Object / Blob Parallel Inheritance Poltergeist fixed by → PATTERN (the fix) DI / Singleton (correct usage) Decorator Strategy / State Factory Method / DI Facade + Mediator Bridge Delete or Facade (with real logic)
Interview tip: When asked “Why would you use [Pattern X]?”, start by describing the anti-pattern it fixes. This demonstrates that you understand the problem first, not just the solution. Interviewers value problem identification over pattern memorization.