Anti-Patterns
The wrong approach for each pattern — how to recognise and fix them.
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.
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
- 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
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 |
Singleton Abuse
AppContext.getInstance().
Why it breaks:
- Hidden dependencies —
OrderServicedepends 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
- 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 (
@Singletonscope) 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)
Subclass Explosion
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
- 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
Giant Conditional
if/else or switch on type that appears in multiple methods across the class. Every new type requires editing every method.
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
- Strategy — extract each branch into a
PaymentStrategyimplementation; 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
Hardcoded Dependencies
new ConcreteClass() scattered throughout the codebase. The class that uses a service also creates it — coupling usage to a specific implementation.
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
newcalls — ifPdfExporter’s constructor changes, you hunt down every instantiation site - Violates Dependency Inversion — high-level module depends on low-level concrete classes
- Factory Method — move
newinto a factory method that subclasses override; the generator works against interfaces only - Dependency Injection — pass
Database,Exporter,Mailerinterfaces through the constructor - Abstract Factory — when you need families of related dependencies that must match (e.g. all AWS or all GCP services)
Blob / God Object
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
- 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
Parallel Inheritance
Shape, you must also add a matching subclass of Renderer. Two hierarchies grow in lockstep — forget one and the system breaks.
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)
- Split into two independent hierarchies connected by composition:
Shapeholds a reference toRenderer Circlecallsrenderer.drawCircle(x, y, radius)— any renderer works with any shape- M shapes + N renderers = M + N classes (not M × N)
Poltergeist
Why it breaks:
- Unnecessary indirection — callers could use
OrderServicedirectly with no loss - Maintenance burden — every new method on
OrderServicerequires 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
- 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.