Question 22 · Section 18

What Anti-patterns Contradict SOLID Principles?

Imagine building a house: an anti-pattern is when all wires from outlets lead to one junction box. It works today, but if you need to add a new outlet — you'll have to redo ever...

Language versions: English Russian Ukrainian

🟢 Junior Level

Anti-patterns are typical design mistakes that seem convenient now but create major problems later. Most of them directly contradict SOLID principles.

Imagine building a house: an anti-pattern is when all wires from outlets lead to one junction box. It works today, but if you need to add a new outlet — you’ll have to redo everything.

Main Anti-patterns

1. God Object One class does everything: works with DB, sends emails, generates reports, processes payments.

  • Violates: SRP — the class has too many responsibilities
  • Sign: A class with 1000+ lines and 30+ dependencies
// God Object — does EVERYTHING
public class ApplicationManager {
    public void createUser(User user) { /* ... */ }
    public void sendEmail(User user) { /* ... */ }
    public void generateReport() { /* ... */ }
    public void processPayment(BigDecimal amount) { /* ... */ }
    public void connectToDatabase() { /* ... */ }
    // 50 more methods...
}

2. Anemic Domain Model Entity classes contain only fields and getters/setters, while all logic is moved to giant services.

  • Violates: Encapsulation — data and logic are separated
// Anemic model — data only, no logic
public class Order {
    private Long id;
    private BigDecimal total;
    private List<OrderItem> items;
    // only getters and setters
}

// All logic here
public class OrderService {
    public BigDecimal calculateTotal(Order order) { /* ... */ }
    public void addItem(Order order, OrderItem item) { /* ... */ }
    public boolean isValid(Order order) { /* ... */ }
}

3. Feature Envy A method of class A constantly accesses data and methods of class B. It “envies” class B and should belong to it.

  • Violates: Encapsulation and SRP
// Feature Envy: method depends on another class's data
public class OrderReport {
    public String formatOrder(Order order) {
        return "Order #" + order.getId()
            + " Total: " + order.getTotal()
            + " Items: " + order.getItems().size()
            + " Customer: " + order.getCustomer().getName();
    }
}
// This method "envies" Order and should be in it

4. Circular Dependency Service A depends on B, and B depends on A.

  • Violates: DIP — impossible to use modules independently
// Circular Dependency: A → B → A
@Service
public class OrderService {
    private final PaymentService paymentService; // depends on PaymentService
}

@Service
public class PaymentService {
    private final OrderService orderService; // depends on OrderService
}

When This Happens

  • In fast-growing startups without code review
  • When the developer doesn’t know SOLID
  • When “we’ll refactor later” never comes

🟡 Middle Level

STUPID — Antonym of SOLID (mnemonic describing anti-patterns opposite to SOLID)

STUPID Description Violates SOLID
Singleton Global state that hinders testing DIP
Tight Coupling Strong module coupling DIP, SRP
Untestability Impossible isolated testing DIP
Premature Optimization Optimization before finding real bottlenecks OCP
Indistinct Names Unclear class and method names SRP
Duplication Code duplication DRY (SOLID’s relative)

Big Ball of Mud

A system without clear architecture, where dependencies are chaotically tangled.

  • Violates: All SOLID principles
  • Sign: Dependency graph looks like a tangled ball of yarn
  • Treatment: Strangler Fig pattern — gradual module replacement

Stovepipe System

Many independent, poorly connected modules that duplicate each other’s functions and have their own data formats.

  • Violates: DIP and ISP
  • Consequence: Impossible integration and huge maintenance costs

Common Mistakes

  1. Mistake: “It works, so the architecture is fine.” Solution: Works today — doesn’t mean it will work tomorrow. Evaluate the cost of adding a new feature.

  2. Mistake: “God Object is just a convenient central class.” Solution: If a class knows about DB, email, PDF, and payments — it becomes a single point of failure and a bottleneck.

  3. Mistake: “Circular Dependency — Spring will handle it via proxies.” Solution: Spring does create proxies, but this masks the problem. Use an event-driven approach or extract a shared interface.

When NOT to Fight Anti-patterns

  • Legacy in maintenance: If the system will be replaced soon, refactoring won’t pay off
  • Prototype: For MVP, speed matters more than architecture
  • One-time script: A utility for one-time data import

Anti-pattern Comparison

Anti-pattern Detection Difficulty Fixing Difficulty Team Impact
God Object Easy (by class size) Medium (refactoring) Blocks parallel work
Anemic Model Medium (needs domain analysis) High (logic migration) Services grow uncontrollably
Circular Dependency Easy (startup error) Medium (event/interface) Spring proxy overhead
Big Ball of Mud Easy (general feeling) Very high Paralyzes development
Feature Envy Medium (needs static analysis) Low (move the method) Confusing code

🔴 Senior Level

Internal Implementation: Why Anti-patterns “Stick”

God Object and Cognitive Load: The human brain can hold 7+/-2 items in working memory. A God Object with 50 methods and 30 fields exceeds this limit 5-10 times. Developers avoid modifying what they can’t understand — the code “freezes.”

Circular Dependency and JVM Classloader:

OrderService.class -> loading -> needs PaymentService
PaymentService.class -> loading -> needs OrderService

Spring works around this through CGLIB proxies: creates a wrapper subclass. But this means:

  • Additional indirection on every call (~1-3ns overhead)
  • this inside the method refers to the proxy, not the real object
  • @Transactional on such methods may not work correctly (proxy-self-invocation problem)

Anemic Model and ORM: Hibernate/JPA require no-arg constructors, getters/setters, equals/hashCode by ID. This pushes toward an anemic model. But this isn’t a “violation” — it’s an architectural trade-off between OOP purity and ORM compatibility.

Architectural Trade-offs

Anemic Domain Model:

  • Pros: ORM compatibility (Hibernate); simple serialization (JSON); easy understanding for junior developers
  • Cons: Business logic spread across services; services grow into God Objects; no encapsulation of invariants

Rich Domain Model:

  • Pros: Logic near data; invariants protected; testability; DDD-compatible
  • Cons: ORM integration complexity; requires a mature team; harder to serialize

Accepting Anti-patterns (Pragmatic):

  • Pros: Development speed; less boilerplate; simpler onboarding
  • Cons: Technical debt grows; in 2 years — Big Ball of Mud; exponential change cost

Edge Cases

  1. Static Utility Classes: Math, Collections, StringUtils — formally violate DIP (can’t replace implementation). But they are stable and domain-independent. Solution: Accept as a valid exception. They don’t change and carry no state.

  2. Framework Annotations: @Entity, @Service, @RestController — framework binding violates DIP. But the framework doesn’t work without them. Solution: Isolate annotations in the Infrastructure Layer. Domain Layer shouldn’t depend on Spring/Hibernate.

  3. DTO Classes: Classes for passing data between layers. Formally — anemic model. But their only responsibility is transport. Solution: Use records (Java 14+) for immutable DTOs. This isn’t an anti-pattern, it’s a pattern.

  4. Partial God Object: A 5000-line class where 4000 lines are generated code (mappers, builders). Solution: Exclude generated code from metrics. SonarQube supports @Generated annotation exclusion.

Performance

God Object and Contention:

Request 1 -> GodObject.methodA() -> lock on fieldX
Request 2 -> GodObject.methodB() -> lock on fieldX (contention!)

God objects often become synchronization points. If 10 threads call different methods of one object with shared mutable state — you’ll get contention. Under high contention, throughput degrades significantly.

Speculative Generality and Memory: The “abstractions in reserve” anti-pattern (creating interfaces and layers for hypothetical future requirements) bloats the class hierarchy:

  • Each interface = class object in metaspace (~1-3KB)
  • 100 unused interfaces = ~100-300KB metaspace
  • Each factory = additional object for DI container
  • In practice: projects with speculative generality have significantly more classes, most with only one implementation

Circular Dependency and Startup Time: Spring on detecting circular dependencies:

  1. Tries constructor injection -> fail
  2. Tries setter injection + CGLIB proxy -> success (with warning)
  3. Proxy creation adds ~5-10ms per dependency
  4. With 50 circular dependencies — +250-500ms to startup time

Production Experience

War Story: Payment Processing System (2022)

Fintech startup, processing 50K payments/day. PaymentManager — God Object at 15,000 lines, 89 dependencies. Worked for 2 years, then:

  • Adding a new payment provider: 3-4 weeks
  • Regression bugs: 5-7 per release
  • Merge conflicts: daily
  • New developer: 3 months to first PR

Refactoring via Strangler Fig:

  1. Extracted PaymentProvider interface from PaymentManager methods
  2. Gradually migrated each provider into a separate class
  3. Orchestrator became a coordinator (300 lines instead of 15,000)
  4. Result: new provider in 2 days, 0 regression bugs per quarter

War Story: Legacy Banking System (2024)

Bank, core on Java 8 (migrated to 17). TransactionProcessor — Big Ball of Mud, 40,000 lines. Dependency graph: 200+ classes, each depends on every other.

Diagnosis:

SonarQube metrics:
- Technical Debt: estimated in years of work to fix (conditional metric)
- Cognitive Complexity: 450 (limit 15)
- LCOM4: 12.0
- Circular Dependencies: 23

Approach:

  1. Froze TransactionProcessor — no more changes
  2. Built Strangler Fig around it: new TransactionEngine with clean SOLID
  3. API Gateway routed: new transaction types -> new engine, old -> legacy
  4. Over 8 months, migrated 100% of traffic
  5. Result: Technical Debt significantly reduced

Monitoring and Diagnostics

  • Static Analysis Tools:
    • SonarQube: Cognitive Complexity, LCOM4, Number of Dependencies, Duplications
    • PMD: God Class detection, Excessive Imports, Coupling Between Objects
    • Checkstyle: Class Data Abstraction Coupling (CDAC)
  • ArchUnit — automatic checks in CI/CD: ```java @ArchTest static final ArchRule no_god_objects = noClasses() .that().resideInAPackage(“..service..”) .should().haveMoreThan(10Dependencies()) .orShould().haveMoreThan(50Methods()) .as(“No God objects in service layer”);

@ArchTest static final ArchRule no_circular_dependencies = slices() .matching(“com.company.(*)..”) .should().beFreeOfCycles() .as(“No circular dependencies between modules”);

@ArchTest static final ArchRule no_feature_envy = noClasses() .that().haveSimpleNameEndingWith(“Service”) .should().accessClassesThat().haveSimpleNameEndingWith(“Entity”) .moreThan(3) .as(“Services should not envy entities too much”); ```

  • Dependency Graph Visualization:
    • JDepend: Coupling and abstraction metrics
    • IntelliJ IDEA Dependency Matrix: Visual graph
    • Structure101: Automatic cycle and layer violation detection
  • Key Metrics: | Metric | Normal | Critical | | ———————- | ——- | ———- | | Dependencies per class | <= 10 | > 20 | | LCOM4 | <= 1 | > 3 | | Cognitive Complexity | <= 15 | > 50 | | Circular dependencies | 0 | > 3 | | Class size (lines) | <= 300 | > 1000 |

Best Practices for Highload

  • Immutability by Default: God Objects almost always have mutable shared state. Make objects immutable — this automatically prevents contention.
  • Event-Driven Decoupling: Instead of direct dependencies between services, use event bus (Kafka, Spring Events). This breaks circular dependencies.
  • Bounded Contexts (DDD): Divide the system by business contexts. God Object violates boundaries — don’t allow cross-context calls directly.
  • Feature Flags for Strangler Fig: When refactoring a God Object, keep old and new code simultaneously. Switch via feature flag, not via deployment.
  • AI-assisted Refactoring: Modern tools (IntelliJ IDEA AI Assistant, CodeRabbit) automatically suggest splitting God Objects based on method co-localization analysis.
  • Module System Enforcement (JPMS): Java 9+ module-info.java formalizes dependencies. Circular dependencies become compile-time errors, not runtime problems.
  • ArchUnit as Code: Architecture tests become first-class citizens in CI/CD. Anti-pattern detection — not a code review opinion, but a failing test.

Summary

  • Anti-patterns are symptoms of “rotten design.” Most of them are direct violations of SOLID.
  • Anemic Domain Model — the most common and debated anti-pattern in Java Enterprise.
  • Fight STUPID code through Code Review culture and automatic static analysis.
  • SOLID — not a religion, but medicine against anti-patterns. Apply pragmatically.
  • The best way to fight Big Ball of Mud is to prevent it from appearing.

🎯 Interview Cheat Sheet

Must know:

  • STUPID — antonym of SOLID: Singleton, Tight Coupling, Untestability, Premature Optimization, Indistinct Names, Duplication
  • God Object — one class does everything, violates SRP, >1000 lines, >30 dependencies
  • Anemic Domain Model — data only, no logic; all logic in giant services
  • Circular Dependency — Spring works around via CGLIB proxies, but this masks a design problem
  • Big Ball of Mud — system without architecture, treated with Strangler Fig (gradual replacement)
  • Feature Envy — method of class A constantly accesses data of class B; should be in B
  • Speculative Generality — “abstractions in reserve,” bloats hierarchy without benefit

Frequent follow-up questions:

  • Why is Circular Dependency a problem? — CGLIB proxies: +1-3ns overhead, this = proxy not real object, @Transactional may not work correctly
  • Is Anemic Model always bad? — No, for ORM (Hibernate) — it’s a trade-off; isolate in Data Access Layer
  • How to refactor Big Ball of Mud? — Freeze old code -> Strangler Fig around it -> API Gateway routes -> gradual migration
  • Anti-pattern metrics? — Dependencies > 20, LCOM4 > 3, Cognitive Complexity > 50, Class size > 1000 lines

Red flags (DO NOT say):

  • “It works, so the architecture is fine” (works today — doesn’t mean maintainable tomorrow)
  • “Circular Dependency — Spring will handle it via proxies” (masks the problem, proxy overhead)
  • “God Object — a convenient central class” (single point of failure, bottleneck, impossible to test)

Related topics:

  • [[1. What is Single Responsibility principle and how to apply it]]
  • [[18. How to refactor God Object]]
  • [[9. Why do we need SOLID principles at all]]
  • [[8. What is Dependency Inversion principle]]