What happens if a class has multiple reasons to change
When a class has multiple reasons to change, it directly violates the first SOLID principle: the Single Responsibility Principle (SRP).
🟢 Junior Level
When a class has multiple reasons to change, it directly violates the first SOLID principle: the Single Responsibility Principle (SRP).
In the classic formulation by Robert C. Martin (Uncle Bob):
“A reason to change is a person, role, or business actor who requests changes to the software system.”
If a single class simultaneously serves the interests of the Chief Financial Officer (CFO), Human Resources (HR), the Database Administrator (DBA), and the Frontend team, it becomes an extremely fragile liability.
Real-World Analogy
Consider a 50-tool Swiss Army Knife:
- If the main blade dulls and you send the knife to the workshop for sharpening, you temporarily lose access to the scissors, screwdriver, and corkscrew.
- If the technician accidentally damages the spring of the scissors while sharpening the blade, you only discover this failure when you attempt to use the scissors days later.
Java Code Example: Multiple Actors in One Class
// Class violates SRP: 4 distinct reasons to change driven by 4 distinct stakeholders!
public class Employee {
private String id;
private BigDecimal salary;
// Reason 1 (Finance / CFO): Compensation and bonus calculation algorithms
public BigDecimal calculatePay() {
return salary.multiply(BigDecimal.valueOf(1.2));
}
// Reason 2 (HR Department): Timesheet, vacation, and work-hour tracking
public String reportHours() {
return "Employee " + id + " logged 40 hours this week";
}
// Reason 3 (DBA / Infrastructure): Database persistence schema
public void saveToDatabase() {
System.out.println("INSERT INTO employees VALUES (" + id + ", " + salary + ")");
}
// Reason 4 (Frontend / Mobile API): Serialization formatting for client consumers
public String toJson() {
return "{\"id\":\"" + id + "\"}";
}
}
Direct Consequences in Everyday Development:
- Fragile Side Effects (The Butterfly Effect): A developer adjusts bonus rounding for Finance, inadvertently altering shared formatting variables and corrupting HR’s monthly payroll export.
- Git Merge Hell: Two independent teams modify
Employee.javaconcurrently in separate branches, resulting in painful, error-prone merge conflicts during pull request integration. - Bloated Unit Tests: Testing a simple compensation formula requires instantiating mock databases, transaction managers, and JSON serializers.
🟡 Middle Level
The 5 Systemic Consequences of Multiple Reasons to Change
┌────────────────────────────────────────────────────────┐
│ Class with 3 Actors │
│ (Finance, HR, Security) │
└───────────────────────────┬────────────────────────────┘
│
┌──────────────────┬───────────────────┼──────────────────┬──────────────────┐
▼ ▼ ▼ ▼ ▼
Cascading Bugs Merge Conflicts Cognitive Overload Bloated Test Mocks Connection Pool
(Side Effects) in Git Repository ("Fear to Touch") (Complex Fixtures) Starvation
- Cascading Side Effects (Fragility): Modifying JSON serialization for a mobile client inadvertently mutates internal entity fields, causing silent data corruption during subsequent database flushes.
- Git Branching Friction (Conway’s Law): When developers from the billing squad and developers from the identity squad constantly touch the same monolithic class, CI/CD pipelines stall due to unending merge conflicts.
- Cognitive Overload & Fear of Touching Code: No engineer can hold 2,500 lines of disparate domain logic in working memory. Teams develop a “fear of touching legacy code”, preferring to hack around existing methods rather than cleanly refactor.
- Extreme Code Churn:
The metric Code Churn measures how frequently a file is edited. When a file like
OrderProcessor.javais modified in $70\%$ of all pull requests, it is a proven hotspot destined for catastrophic production regressions. - Database Connection Pool Exhaustion:
When a class bundles database operations and third-party network calls inside a single method, developers often mark the method
@Transactional. If the external HTTP call suffers network latency (e.g., a 20-second timeout), the physical database connection from the HikariCP pool remains blocked and idle, starving the application and causing total downtime (ConnectionTimeoutException).
🔴 Senior Level
Hardware and JVM Degradation
Monolithic classes aggregating multiple responsibilities create hidden hardware and runtime performance bottlenecks:
1. Hardware False Sharing in CPU Cache Lines
In a multi-responsibility class, fields supporting different operational domains reside in the same physical object allocation:
public class MonolithicOrder {
// Accessed and mutated by order-processing worker threads:
public volatile long processedCounter;
// Accessed and mutated by background telemetry and heartbeat threads:
public volatile long heartbeatTimestamp;
}
Both volatile fields are allocated adjacent to each other on the exact same 64-byte CPU Cache Line:
- Core 1 increments
processedCounter. - The CPU cache coherence protocol (MESI/MOESI) immediately invalidates the entire 64-byte cache line across all other CPU cores.
- Core 2 (attempting to read or update
heartbeatTimestamp) suffers a cache miss and must re-fetch from main RAM. - Inter-thread throughput plummets by $10\times$ to $50\times$ due to continuous cache line bouncing.
2. Lock Contention on Shared Monitors
If different methods in the class utilize synchronized blocks or methods, background reporting threads block customer-facing checkout transactions because both compete for the same object monitor (this).
3. JIT Inlining and Devirtualization Failures
HotSpot C2 enforces bytecode size thresholds (e.g., -XX:MaxInlineSize=35, -XX:FreqInlineSize=325). Massive God Classes with complex control flow exceed these compiler thresholds. The JIT compiler abandons method inlining and escape analysis, preventing scalar replacement and forcing heap allocation for transient variables.
Architectural Solution: Orchestrator + Facade Pattern
To eliminate multiple reasons to change, isolate domain rules into focused services and orchestrate them through a clean, logic-free coordinator:
// 1. Narrow domain services: Exactly 1 reason to change (1 actor)
@Service
public class PayrollCalculator {
public BigDecimal calculateSalary(Employee employee) { /* Pure financial math */ }
}
@Service
public class EmployeeRepository {
public void persist(Employee employee) { /* Pure SQL / JPA transactions */ }
}
@Service
public class EmployeeNotifier {
public void notifyHired(Employee employee) { /* Pure notification dispatch */ }
}
// 2. Orchestrator: Coordinates workflow without containing math, SQL, or transport logic
@Service
public class OnboardingOrchestrator {
private final PayrollCalculator calculator;
private final EmployeeRepository repository;
private final EmployeeNotifier notifier;
public OnboardingOrchestrator(PayrollCalculator calc, EmployeeRepository repo, EmployeeNotifier notif) {
this.calculator = calc;
this.repository = repo;
this.notifier = notif;
}
public void onboardNewEmployee(Employee employee) {
calculator.calculateSalary(employee);
repository.persist(employee);
notifier.notifyHired(employee);
}
}
4 Tricky Questions
1. How does Conway’s Law explain the operational friction of a class with multiple reasons to change?
Answer: Conway’s Law states: “Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.”
When a single class possesses multiple reasons to change, it attempts to merge requirements originating from multiple independent business units (e.g., Accounting, Sales, Infrastructure). Because these business units operate with separate priorities and communication cadences in real life, forcing their requirements into a single codebase artifact inevitably causes conflicting business rules, merge battles, and regression defects. SRP resolves this by aligning class boundaries with organizational bounded contexts.
2. Does a generic UserDTO containing 40 fields for all frontend screens violate SRP?
Answer: Yes. This is the Fat DTO / Generic DTO anti-pattern.
A 40-field UserDTO changes whenever the Profile screen, Security Settings screen, Billing screen, or Shipping screen changes.
- It forces over-fetching of sensitive data across the wire (e.g., sending password hashes or payment tokens to the profile screen).
- Any field format alteration requested by the mobile team risks breaking web client deserialization.
Solution: Decompose into specialized, role-based DTOs: UserProfileViewDto, UserSecuritySettingsDto, and UserBillingAddressDto.
3. Why does bundling multiple responsibilities almost always lead to database connection pool exhaustion?
Answer:
When a method bundles persistence logic (repository responsibility) with third-party network calls (HTTP client responsibility), developers frequently annotate the outer method with @Transactional.
A database connection is leased from the HikariCP pool at the start of the method. When the thread issues an external HTTP REST call, network latency, remote service throttling, or socket timeouts (e.g., 15–30 seconds) freeze the thread while the database socket remains open and locked. Under moderate traffic, all connections in the pool are consumed within seconds, paralyzing the entire database tier (ConnectionTimeoutException).
4. How can you mathematically prove to engineering management that a monolithic class requires refactoring?
Answer: By running Code Churn and Hotspot Analysis on the version control history:
- Execute repository analytics:
git log --format=format: --name-only --since="1 year ago" | sort | uniq -c | sort -nr | head -20 - Correlate churn with bug trackers (Jira/GitHub Issues): calculate the percentage of historical regression tickets linked to commits in that file.
- Quantify developer time lost resolving merge conflicts and re-running long-running test suites. Presenting an empirical correlation between high file churn and production incidents provides indisputable business justification for an SRP refactoring initiative.
🎯 Interview Cheat Sheet
30-Second Summary
- Root Cause: A class has multiple reasons to change when it answers to multiple distinct business actors or stakeholders.
- Primary Symptoms: Cascading regressions (Butterfly Effect), high Git merge friction, fear of touching legacy code, and bloated test suites.
- Hardware & JVM Hazards: False Sharing on 64-byte CPU cache lines across worker threads, HikariCP connection starvation inside
@Transactional, and JIT inlining failures. - Target Architecture: Decompose logic into focused, single-actor services and coordinate them using an Orchestrator or Facade.
Common Follow-up Questions
- “What is the difference between a technical change and a business reason to change?” → Migrating from Java 17 to Java 21 is a platform upgrade; a reason to change in SRP strictly represents a business stakeholder demanding modified functional behavior.
- “How does LCOM4 detect multiple reasons to change?” → $LCOM4 \ge 2$ mathematically proves that a class contains disconnected clusters of methods operating on separate state fields, indicating distinct responsibilities.
Red Flags (DO NOT Say)
- ❌ “A class has multiple reasons to change if it has more than 10 methods.” (Method count is irrelevant; what matters is whether those methods serve one actor or multiple actors).
- ❌ “Extracting methods into static utility classes solves the multiple reasons to change.” (Static utility dumping grounds merely disguise low cohesion; it does not solve the architectural boundary problem).
- ❌ “Git merge conflicts cannot be mitigated by software architecture.” (High merge conflict frequency is directly caused by low cohesion and poor SRP boundaries).
Related Topics
- What is Single Responsibility Principle and How to Apply It — Fundamental SRP rules
- How is Single Responsibility related to cohesion — Cohesion and LCOM metrics
- How to refactor God Object — Refactoring large multi-responsibility classes
- How to determine if a class has single responsibility — Heuristic tests