What Happens if a Class Has Multiple Reasons to Change
When a class has multiple reasons to change (SRP violation), it becomes fragile and complex. Any small change can break completely unrelated functionality.
🟢 Junior Level
When a class has multiple reasons to change (SRP violation), it becomes fragile and complex. Any small change can break completely unrelated functionality.
Simple analogy: Imagine a Swiss Army knife with 50 tools. If you need to replace the blade — you have to disassemble the entire knife. And if you break the screwdriver — the corkscrew stops working too.
Violation example:
// Bad: 4 different reasons to change
public class User {
private String name; // Data
public void validate() { } // Validation (changed by lawyers)
public void save() { } // Persistence (changed by DBAs)
public String toJson() { } // Format (changed by frontend)
}
What will happen:
- Changing validation → may break persistence, because both methods share the class’s fields
- Changing JSON format → need to retest everything, since the code is in one file
- Different teams conflict over one file during merges
How to fix:
// Good: each responsibility in its own class
public class User { private String name; } // Data only
public class UserValidator { void validate(User u) { } } // Validation only
public class UserRepository { void save(User u) { } } // DB only
public class UserMapper { String toJson(User u) { } } // Format only
🟡 Middle Level
How It Works
When a class has multiple reasons to change, it becomes “Rigid” and “Fragile”. Each responsibility is an “axis of change.” If there are several — the class collapses under pressure.
Consequences for the Project
| Problem | Description | Impact |
|---|---|---|
| Cognitive Load | Class with 5 responsibilities = hundreds of combinations | Developers are afraid to change the code |
| Fragility | Change in one place breaks another | Cascading bugs |
| Merge Conflicts | Different teams change the same file | In large projects — a significant portion of time goes into resolving conflicts |
Practical Application
Cognitive Complexity Metric (SonarQube):
- Method > 15 → hard to maintain
- Class > 200 lines → difficult to hold all logic in mind, bug risk grows
Code Churn Analysis (metric of how frequently a file changes):
- If a file appears in most commits → too many reasons to change
Common Mistakes
| Mistake | Solution |
|---|---|
| God Object (a class with dozens of fields and methods for every occasion) | Split by responsibilities |
| Butterfly Effect (1 change → 10 edits in other files) | Extract separate services |
| Daily merge conflicts | Package-by-Feature (group by features, not layers) |
When NOT to Strictly Follow SRP
- Prototypes/MVPs (quick validation)
- Migration scripts (one-time code)
- When the cost of change is minimal
🔴 Senior Level
Internal Implementation: The Mechanics of Fragility
Shared State Problem:
class GodObject {
private Date lastProcessed; // Used for logging AND business logic
// Changing the date format in logs breaks calculations
}
Memory Layout Issues:
- False Sharing: Fields of different responsibilities fall into the same cache line (64 bytes)
- Impact: CPU cores invalidate each other’s cache → slowdown 10-100x
Architectural Trade-offs
Strict SRP:
- ✅ Pros: Minimal side effects, parallel development, easy testing
- ❌ Cons: Class explosion, navigation complexity, communication overhead
Moderate SRP:
- ✅ Pros: Balance between purity and practicality
- ❌ Cons: Requires mature judgment, risk of gradual architectural degradation
Edge Cases
- Lock Contention: Singleton God Object → different threads compete for one monitor
- Solution: Split into separate beans with independent locks
- Metaspace Impact: Huge classes → more bytecode → more metaspace
- Impact: +2-5MB per 5000+ line class
- Transaction Boundaries: One class coordinates a transaction across multiple domains
- Solution: Orchestrator pattern (a coordinator class that calls separate services in the correct order but doesn’t contain their business logic),
@Transactionalon the orchestrator
- Solution: Orchestrator pattern (a coordinator class that calls separate services in the correct order but doesn’t contain their business logic),
Performance
| Metric | God Object | SRP Compliant |
|---|---|---|
| Cognitive Complexity | 50-200+ | <15 |
| L1 Cache Hit Rate | 40-60% | 85-95% |
| Lock Contention | High (single monitor) | Low (independent locks) |
| Test Execution | Slow (many mocks) | Fast (isolated) |
False Sharing Impact:
Field A (logging) and Field B (business logic) in the same cache line
→ Thread 1 writes A → invalidates Thread 2's cache for B
→ 10-100x slowdown under high contention
Thread Safety
- Single monitor bottleneck: All threads wait for the one God Object
- Independent services: Different locks → parallel execution
- Contention rate: God Object → 60-80% wait time, SRP → <10%
Production Experience
BillingEngine Refactoring (15,000 lines):
Problem:
- 8 responsibilities: calculation, validation, DB, email, PDF, audit, cache, logging
- Cognitive Complexity: 340 (norm < 15 — code confusion metric from SonarQube)
- Merge conflicts: daily, 2-3 hours to resolve
- Bug rate: 40% of deployments with bugs
Solution (6 sprints):
- Feature Analysis: grouped methods by domain
- Extracted:
TaxCalculator,InvoiceGenerator,PaymentProcessor - Created
BillingOrchestratorfor coordination - Event-driven:
BillingCompletedEventfor audit/emails
Result:
- Cognitive Complexity: 8-12 per class
- Merge conflicts: <1 per month
- Bug rate: 8% of deployments
- Deployment time: 30 min → 5 min
Monitoring
ArchUnit:
@ArchTest
static void no_god_objects = classes()
.should().haveLessThanNMethods(30)
.andShould().haveLessThanNFields(15)
.andShould().haveLessThanNDependencies(7);
SonarQube:
- Cognitive Complexity > 15 → Code Smell
- Class size > 300 lines → Refactoring candidate
Git Analysis:
# File change frequency
git log --oneline -- User.java | wc -l
# If >90% of commits touch this file → SRP violation
Best Practices for Highload
- Max 300 lines per class
- Max 15 fields per class
- Max 7 dependencies
- Package-by-Feature for modularity
- ArchUnit CI checks
- Code churn monitoring in Git
🎯 Interview Cheat Sheet
Must know:
- Class with multiple reasons to change = “Rigid” and “Fragile”
- Cognitive Load: hundreds of combinations → developers afraid to change code
- Butterfly Effect: 1 change → 10 edits in other files
- False Sharing: fields of different responsibilities in the same cache line → slowdown 10-100x
- God Object → single monitor bottleneck → 60-80% wait time in multi-threaded environments
- Orchestrator pattern solves the problem: coordination without business logic
- Metrics: Cognitive Complexity > 15, Class size > 300 lines, Dependencies > 7
Frequent follow-up questions:
- What is the Shared State Problem? — A field used for different purposes (logging AND business logic), changing format breaks calculations
- How does God Object affect Thread Safety? — One monitor for the entire object → all threads wait, contention rate 60-80%
- What is Code Churn? — Metric of file change frequency; >90% of commits touching a file → SRP violation
- What is the refactoring result? — Cognitive Complexity 8-12, bug rate 8%, deployment time 5 min instead of 30
Red flags (DO NOT say):
- “A small SRP violation is fine” (cascading bugs and merge conflicts accumulate)
- “God Object can be fixed in one sprint” (real cases: 6 sprints for 15,000 lines)
- “SRP is only for large classes” (a 30-line class can do 3 things)
Related topics:
- [[1. What is Single Responsibility principle and how to apply it]]
- [[13. How is Single Responsibility related to cohesion]]
- [[18. How to refactor God Object]]
- [[21. How to determine if a class has single responsibility]]