📐 Section 18 · Question #22

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:

  1. Startup Failures in Spring Boot 2.6+: Spring cannot construct a Directed Acyclic Graph (DAG) for bean initialization, throwing BeanCurrentlyInCreationException.
  2. The @Lazy Hack and CGLIB Proxy Degradation: Desperate engineers often plaster @Lazy across 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 @Transactional or @Async inside the same class bypasses the CGLIB proxy, causing transactions to silently fail without rolling back on exceptions!
  3. 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 Service classes.
    • 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:

  1. 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.
  2. 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.


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:

  1. 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.
  2. IDE Clutter: Navigating to method implementations requires an extra click to bypass the interface.
  3. ISP Violation over Time: Because developers view IUserService as “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 / ServiceImpl 1-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 @Lazy is the best practice for fixing Spring circular dependencies.” (@Lazy is a temporary bandage that masks severe architectural coupling).