What anti-patterns contradict SOLID principles
An Anti-Pattern is a recurring software design mistake that initially appears expedient, but ultimately leads to code rot, severe architectural fragility, testability hurdles, a...
🟢 Junior Level
An Anti-Pattern is a recurring software design mistake that initially appears expedient, but ultimately leads to code rot, severe architectural fragility, testability hurdles, and exponential maintenance costs.
Each of the five SOLID principles was conceived as a direct architectural antidote to a specific category of dangerous anti-patterns:
| SOLID | Direct Antidote For | Code Smell & Structural Defect |
|---|---|---|
| S (SRP) | God Object (The Blob) | Monolithic class (2,000+ LOC) doing everything. |
| O (OCP) | Hardcoded Type Conditionals | Chained if (type == X) or sprawling switch. |
| L (LSP) | Refusal of Bequest | Subclass throwing UnsupportedOperationEx. |
| I (ISP) | Fat / Polluted Interface | 40-method interface forcing empty dummy mocks. |
| D (DIP) | Static Cling & Hardcoded New | Hardwired new Service() inside business code. |
The STUPID Mnemonic: The Direct Antagonist of SOLID
In object-oriented design, the STUPID mnemonic categorizes the most common procedural bad habits that undermine OOP:
- S — Singleton Overuse: Relying on global mutable state via static singletons.
- T — Tight Coupling: Classes hardwired directly to concrete implementation details.
- U — Untestability: Inability to write isolated unit tests without booting full infrastructure.
- P — Premature Optimization: Sacrificing clean abstractions for speculative, unmeasured micro-optimizations.
- I — Indistinct / Vague Naming: Cluttering code with meaningless names (
AppManager,DataProcessor,Helper). - D — Duplication: Violating DRY and creating shotgun surgery requirements.
🟡 Middle Level
The Comprehensive SOLID vs. Anti-Pattern Taxonomy
| SOLID Principle | Protected Architectural Invariant | Opposing Anti-Patterns | Code Symptoms in Production |
|---|---|---|---|
| S (SRP) | One class = One business actor (single reason to change). | God Object (The Blob) Feature Envy Accidental Duplication |
Class imports SQL, Mail, and REST libraries simultaneously; method constantly reads getters of foreign objects; LCOM4 $\ge 2$. |
| O (OCP) | Open for extension, closed for modification. | Type Code with Switch Speculative Generality |
Chained instanceof statements; frequent Git merge wars in core files; or the opposite extreme: 12 interfaces with only 1 implementation. |
| L (LSP) | Subtypes must be behaviorally substitutable for base types. | Refusal of Bequest Square-Rectangle Trap Broken Invariants |
Overridden methods throwing UnsupportedOperationException; subtyping alters base invariants; broken equals/hashCode symmetry. |
| I (ISP) | Clients should not depend on methods they do not call. | Fat Interface Header Interface ( IService)Interface Pollution |
Monolithic interfaces forcing empty {} method stubs; mechanical IUserService with only one UserServiceImpl. |
| D (DIP) | High-level business logic must depend on abstractions. | Static Cling Service Locator Circular Dependency |
new ConcreteRepo() inside domain logic; ServiceLocator.getBean(); mutual reference loops ($A \to B \to A$). |
Deep Dive: The Feature Envy Anti-Pattern
One of Martin Fowler’s most prevalent code smells:
// ❌ FEATURE ENVY: OrderProcessor is "envious" of User's internal Wallet
public class OrderProcessor {
public void processPayment(User user, BigDecimal amount) {
// Method reaches into User and Wallet, performing calculations on foreign state:
if (user.getWallet().getBalance().compareTo(amount) >= 0) {
BigDecimal newBalance = user.getWallet().getBalance().subtract(amount);
user.getWallet().setBalance(newBalance); // Destroys Wallet encapsulation!
} else {
throw new InsufficientFundsException();
}
}
}
Why It Violates SRP and Encapsulation:
The rules of balance deduction belong strictly inside Wallet, not inside OrderProcessor.
The “Tell, Don’t Ask” Fix:
// ✅ ENCAPSULATED DOMAIN OPERATION:
user.getWallet().withdraw(amount); // Wallet enforces its own invariants internally
🔴 Senior Level
Circular Dependencies ($A \leftrightarrow B$) and Spring CGLIB Traps
The Circular Dependency anti-pattern is a direct consequence of violating both DIP and SRP:
@Service
public class OrderService {
private final PaymentService paymentService;
}
@Service
public class PaymentService {
private final OrderService orderService; // 💥 CIRCULAR DEPENDENCY CYCLE!
}
High-Severity Technical Consequences:
- Startup Failures in Spring Boot 2.6+:
Spring cannot construct a Directed Acyclic Graph (DAG) for bean initialization, throwing
BeanCurrentlyInCreationException. - The
@LazyHack and CGLIB Proxy Degradation: Desperate engineers often plaster@Lazyacross constructor arguments.- Spring injects a dynamic CGLIB proxy instead of the real bean.
- Every method call incurs dynamic proxy interceptor overhead (~30–50 ns).
- The Self-Invocation Proxy Trap: Calling an internal method annotated with
@Transactionalor@Asyncinside the same class bypasses the CGLIB proxy, causing transactions to silently fail without rolling back on exceptions!
- Senior Architectural Fix:
- Decouple using the Mediator Pattern (extract a coordinating service).
- Convert synchronous coupling to asynchronous Domain Events via Spring’s
ApplicationEventPublisher.
Architectural Dilemma: Anemic Domain Model vs. Rich Domain Model
A central debate in enterprise software design:
- Anemic Domain Model (Martin Fowler):
Entities are dumb DTOs consisting only of private fields, getters, and setters. All business rules live in procedural
Serviceclasses.- Benefits: Frictionless compatibility with JPA/Hibernate, low initial cognitive barrier for junior engineers.
- Liabilities: Complete breakdown of object-oriented encapsulation; service classes quickly metastasize into untestable multi-thousand-line God Objects.
- Rich Domain Model (Domain-Driven Design - Evans):
Entities encapsulate data alongside the business invariants that govern that data (
order.cancel(),order.applyDiscount()).- Benefits: High cohesion, strict adherence to SRP at the Aggregate boundary, self-validating state.
- Liabilities: Requires careful mapping separation between persistence entities (JPA) and domain aggregates.
System-Wide Architectural Rot: Big Ball of Mud & Stovepipes
When anti-patterns are allowed to proliferate unchecked:
- Big Ball of Mud (Foote & Yoder): A haphazard architecture where structural boundaries are completely eroded. Any class can implicitly depend on any other class. Direct refactoring is impossible; the only remedy is wrapping the monolith in facades and carving out microservices via the Strangler Fig Pattern.
- Stovepipe Enterprise Architecture: Multiple separate applications or monoliths within the same enterprise duplicating the exact same business functions with incompatible data schemas and zero shared abstractions (severe enterprise-level ISP and DIP failure).
4 Tricky Questions
1. Is the classic GoF Singleton pattern an unconditional anti-pattern in modern Java?
Answer:
The classic Gang of Four (GoF) Singleton (public static getInstance() with a private static field) is widely recognized as an anti-pattern in modern enterprise software. It introduces global mutable state, tightly couples callers to a concrete class, hides dependencies, and creates test concurrency hazards (state leakage between parallel JUnit tests).
However, the Singleton Lifetime Scope in Dependency Injection containers (Spring Singleton Scope) is a fundamental architectural pattern. The container manages a single instance of a bean, injects it via constructor parameters (adhering to DIP), and allows trivial replacement with Mockito mocks during testing.
2. How is the Speculative Generality anti-pattern related to an over-application of OCP?
Answer: Speculative Generality represents OCP distorted into harmful over-engineering. An engineer attempts to anticipate every theoretical feature request for the next five years, constructing 10 layers of abstract factories, strategy registries, and generic interfaces for a subsystem that will only ever have a single implementation.
This burdens the team with cognitive navigation friction, violates YAGNI, and bloats the codebase. Pragmatic OCP dictates building extension points only when variation is empirically confirmed by product roadmaps or when adhering to the Rule of Three.
3. What is the fundamental difference between Feature Envy and a Law of Demeter violation?
Answer:
- Feature Envy: A method resides in the wrong class and spends more time accessing the fields and getters of another class than its own data. The flaw is method location: the calculation should be moved inside the target class.
- Law of Demeter (Train Wreck): A method navigates deeply through foreign object topologies across multiple hops (
a.getB().getC().getD().run()). The flaw is structural navigation: the caller knows the internal composition hierarchy far beyond its immediate neighbor.
Both anti-patterns are resolved by applying the “Tell, Don’t Ask” principle.
4. Why is the Header Interface anti-pattern (IUserService with only UserServiceImpl) considered harmful?
Answer:
- False Decoupling: The interface merely duplicates every method of the concrete class line-for-line. Whenever the implementation changes, the interface must be edited in lockstep.
- IDE Clutter: Navigating to method implementations requires an extra click to bypass the interface.
- ISP Violation over Time: Because developers view
IUserServiceas “the” service interface, it swells to 50+ methods, forcing all clients to depend on a massive fat interface.
Rule: Introduce an interface only when multiple concrete implementations exist (e.g., S3StorageService and LocalStorageService), or when crossing a strict architectural boundary.
🎯 Interview Cheat Sheet
30-Second Summary
- Anti-Patterns vs. SOLID:
- SRP Opponent: God Object (The Blob), Feature Envy, Anemic Model.
- OCP Opponent: Hardcoded Switch / Magic Conditionals, Speculative Generality.
- LSP Opponent: Refusal of Bequest (
UnsupportedOperationException), Broken Invariants. - ISP Opponent: Fat Interface, Header Interface (
IService/ServiceImpl1-to-1). - DIP Opponent: Hardcoded
new, Service Locator, Circular Dependencies.
- The STUPID Mnemonic: Singleton (overuse), Tight Coupling, Untestability, Premature Optimization, Indistinct Names, Duplication.
- Circular Dependencies: Break $A \leftrightarrow B$ cycles with Domain Events or Mediator services instead of masking them with
@Lazy.
Common Follow-up Questions
- “How do you diagnose Feature Envy in code reviews?” → Look for methods where an external object’s getters are invoked 4+ times to calculate a value. Move the method into that external object.
- “Why is an Anemic Domain Model risky in high-complexity domains?” → Without behavior inside entities, business validation rules are scattered across 20 services, making it impossible to guarantee data integrity.
Red Flags (DO NOT Say)
- ❌ “Singletons with static
getInstance()are fine for database connection pools.” (Global mutable state destroys test isolation; use Spring DI beans instead). - ❌ “Every class should implement an interface with the same name prefixed by
I.” (This is the Header Interface anti-pattern). - ❌ “Using
@Lazyis the best practice for fixing Spring circular dependencies.” (@Lazyis a temporary bandage that masks severe architectural coupling).
Related Topics
- What is Single Responsibility Principle and How to Apply It — Antidote to God Objects
- How to refactor God Object — Step-by-step Blob refactoring
- Why Do We Need SOLID Principles at All — Total cost of software ownership
- What is Dependency Inversion Principle — Decoupling from concrete details