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...
🟢 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
-
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.
-
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.
-
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)
thisinside the method refers to the proxy, not the real object@Transactionalon 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
-
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. -
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. -
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.
-
Partial God Object: A 5000-line class where 4000 lines are generated code (mappers, builders). Solution: Exclude generated code from metrics. SonarQube supports
@Generatedannotation 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:
- Tries constructor injection -> fail
- Tries setter injection + CGLIB proxy -> success (with warning)
- Proxy creation adds ~5-10ms per dependency
- 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:
- Extracted
PaymentProviderinterface fromPaymentManagermethods - Gradually migrated each provider into a separate class
- Orchestrator became a coordinator (300 lines instead of 15,000)
- 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:
- Froze
TransactionProcessor— no more changes - Built Strangler Fig around it: new
TransactionEnginewith clean SOLID - API Gateway routed: new transaction types -> new engine, old -> legacy
- Over 8 months, migrated 100% of traffic
- 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.
Future Trends
- 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.javaformalizes 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,@Transactionalmay 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]]