Question 14 · Section 18

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.

Language versions: English Russian Ukrainian

🟢 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

  1. Lock Contention: Singleton God Object → different threads compete for one monitor
    • Solution: Split into separate beans with independent locks
  2. Metaspace Impact: Huge classes → more bytecode → more metaspace
    • Impact: +2-5MB per 5000+ line class
  3. 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), @Transactional on the orchestrator

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):

  1. Feature Analysis: grouped methods by domain
  2. Extracted: TaxCalculator, InvoiceGenerator, PaymentProcessor
  3. Created BillingOrchestrator for coordination
  4. Event-driven: BillingCompletedEvent for 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]]