How to Determine if a Class Has Single Responsibility?
Imagine an office: the accountant calculates salaries, the lawyer checks contracts, the sysadmin configures servers. If one person tries to do everything at once — quality drops.
🟢 Junior Level
Single Responsibility (SRP) means a class should do one thing. If you can describe what a class does in one simple sentence without words like “and,” “or,” “also” — the responsibility is likely single.
Imagine an office: the accountant calculates salaries, the lawyer checks contracts, the sysadmin configures servers. If one person tries to do everything at once — quality drops.
SRP Violation Example:
// This class does too much: validates, saves, and sends email
public class OrderProcessor {
public void process(Order order) {
validate(order); // Validation — validator's responsibility
save(order); // Persistence — repository's responsibility
sendEmail(order); // Notification — notification service's responsibility
generateInvoice(order);// PDF generation — report generator's responsibility
}
}
SRP-Compliant Example:
// Each class — one responsibility
public class OrderValidator {
public void validate(Order order) {
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order is empty");
}
}
}
public class OrderRepository {
public void save(Order order) {
// save to DB
}
}
public class EmailNotificationService {
public void sendConfirmation(Order order) {
// send email
}
}
// Orchestrator only coordinates
public class OrderService {
private final OrderValidator validator;
private final OrderRepository repository;
private final EmailNotificationService notifier;
public OrderService(OrderValidator validator,
OrderRepository repository,
EmailNotificationService notifier) {
this.validator = validator;
this.repository = repository;
this.notifier = notifier;
}
public void process(Order order) {
validator.validate(order);
repository.save(order);
notifier.sendConfirmation(order);
}
}
Simple SRP Test: Try to describe the class’s purpose in one sentence. If you use the word “and” — the class has multiple responsibilities.
🟡 Middle Level
5 Tests for Determining SRP
1. The Naming Test
Describe what the class does without using “and,” “or,” “also,” “too.”
| Class | Description | Result |
|---|---|---|
OrderProcessor |
“Validates order AND saves AND sends email” | ❌ 3 responsibilities |
OrderValidator |
“Validates order correctness” | ✅ 1 responsibility |
PdfGenerator |
“Generates PDF documents” | ✅ 1 responsibility |
UserManager |
“Manages users” — but what exactly? | ❌ Vague |
2. The Change Test
The most precise definition from Robert Martin. Ask: “Who might come and demand changes to this class?”
If it’s lawyers (contract text), accountants (calculation formula), and admins (log path) — the class has at least 3 responsibilities.
3. The Import Test
Look at the class’s imports:
import java.sql.*; // DB
import javax.mail.*; // Email
import com.fasterxml.jackson.*; // JSON
import org.springframework.web.*; // Web
If one class has java.sql, javax.mail, and com.fasterxml.jackson — it’s “spread” across DB, networking, and serialization.
4. The Mocking Test
Try to write a unit test for one method. If testing one if requires setting up 5-7 mocks — the class is overloaded with others’ responsibilities.
// Bad: 7 mocks for one test
@Mock private OrderRepository repo;
@Mock private EmailService email;
@Mock private SmsService sms;
@Mock private PdfGenerator pdf;
@Mock private TaxCalculator tax;
@Mock private DiscountService discount;
@Mock private AuditLogger audit;
// Good: 1-2 mocks, testing one responsibility
@Mock private OrderRepository repo;
@Test void shouldSaveValidOrder() { ... }
5. LCOM4 Cohesion Metric
LCOM4 (Lack of Cohesion in Methods) is computed by static analyzers:
- Imagine a graph where nodes are methods and fields, edges are method-field usage
- If the graph splits into 2+ disconnected components — independent “bodies” live inside one class
| LCOM4 | Meaning |
|---|---|
| 1 | ✅ High cohesion, single responsibility |
| 2-3 | ⚠️ Class can be split |
| 4+ | ❌ Class definitely needs splitting |
Tools: SonarQube, jdepend, IntelliJ IDEA (built-in analysis).
Social Factor of SRP
An interesting perspective: “One class — one developer team.” If people from different teams (Billing team and Delivery team) work on one class — it becomes a bottleneck. Split by team responsibility boundaries.
Common Mistakes
-
Mistake: “The class is small — so SRP is followed.” Solution: Size doesn’t determine responsibility. A 30-line class can do 3 things. A 300-line class can do one complex thing.
-
Mistake: “The class has one public method — so SRP is followed.” Solution: One method can delegate to 10 different subsystems. Look at cohesion, not method count.
-
Mistake: Anemic model (only getters/setters) — this isn’t an SRP violation, it’s shifting responsibility to services. Solution: In classic OOP, logic lives near data. In Enterprise Java, anemic model is often justified (ORM, DI).
When NOT to Strictly Follow SRP
- DTO / Value Objects: Data-only classes — their responsibility is “storing data,” that’s fine
- Test Utilities: Test fixtures, builders — may contain heterogeneous logic
- Small Utilities: A 20-line class with two related methods doesn’t need splitting
Approach Comparison
| Approach | Pros | Minuses | When to Use |
|---|---|---|---|
| Strict SRP (each operation — separate class) | Maximum testability | Class explosion, navigation difficulty | Safety-critical systems (aviation, medicine), where the cost of error is extremely high |
| Reasonable SRP (1 domain = 1 class) | Balance | Requires mature judgment | Most enterprise projects |
| Without SRP | Fast at start | Unmaintainable within a year | Prototypes, one-time scripts |
🔴 Senior Level
Internal Implementation: How the JVM Sees SRP
At the JVM level, SRP doesn’t exist — it’s an architectural constraint. But the consequences of SRP violations are measurable:
Memory footprint: A class with 5 responsibilities = 5 groups of fields rarely used simultaneously. This means:
- Object occupies 200+ bytes, but only 20 bytes are used in a given context
- The remaining 180 bytes are “dead weight” in the CPU cache (cache line pollution)
- For L1 cache (32-48KB per core), this means less useful data per cache line
GC implications: Large object with many fields:
- Promoted to old generation faster (larger size, fills tenured quicker)
- With partial usage — wasted memory in old gen
- G1 GC allocates regions of 1-32MB. A large class with many references can “hook” several regions
JIT compilation: A class with 50 methods and 30 fields:
- JIT can’t optimize effectively — too many execution paths
- Methods are rarely called together — no benefit from compilation batching
- Code cache fragmentation — different “responsibilities” compile separately but occupy adjacent areas
Architectural Trade-offs
Strict SRP (each operation — separate class):
- ✅ Pros: Change isolation; minimal blast radius; each class tested separately; easy to mock
- ❌ Cons: Class explosion (5000+ classes); navigation difficulty; object creation overhead; hard to trace flow
Moderate SRP (grouping by domain context):
- ✅ Pros: Balance between readability and isolation; logically related operations together
- ❌ Cons: Subjective boundaries; possible “creeping” responsibilities
Hybrid SRP (strict in domain, moderate in infrastructure):
- ✅ Pros: Clean domain model; pragmatism in infrastructure
- ❌ Cons: Inconsistency; need to remember which style applies where
Edge Cases
-
Anemic Domain Model:
UserandOrderwith only getters/setters. Formally — not an SRP violation (one responsibility: data storage). But all business logic moves to services, which become God Objects. Solution: In DDD, use Rich Domain Model. In Transaction Script — accept anemic model but control service sizes. -
Class Explosion: 50 classes per SRP for a simple CRUD operation. Solution: Don’t be afraid to create many classes (IDEs handle 10,000+ files). Fear one 10,000-line file. But if a 3-method class does one thing — don’t split it.
-
God Class with a “Good” Name:
PaymentOrchestratorat 5000 lines with 30 dependencies. The name sounds like “one responsibility” (orchestration), but it’s a mask. Solution: Use the Dependency Count metric (counting how many classes a given class depends on). If a class depends on 10+ other classes — it’s a God Class. -
Cross-cutting Concerns (logging, metrics, tracing — needed across many classes): Logging, metrics, tracing — responsibilities “spread” across all classes. Solution: Use aspects (AOP — aspect-oriented programming), decorators, or middleware. Don’t mix cross-cutting concerns with business logic in one class.
// Bad: logging is part of the service's responsibility
public class OrderService {
private final Logger log = LoggerFactory.getLogger(OrderService.class);
private final MeterRegistry metrics;
public Order process(OrderRequest req) {
log.info("Processing order {}", req.getId());
metrics.counter("orders.processed").increment();
// business logic...
log.info("Order {} processed", req.getId());
}
}
// Good: cross-cutting concerns via decorator
public class MeteredOrderService implements OrderService {
private final OrderService delegate;
private final MeterRegistry metrics;
@Override
public Order process(OrderRequest req) {
metrics.counter("orders.processed").increment();
return delegate.process(req);
}
}
Performance
Comparison for an order processing system (100K orders/hour):
| SRP Approach | Class Size (average) | Memory per Object | CPU Cache Hit Rate | GC Pressure |
|---|---|---|---|---|
| God Class (1 class, 5000 lines) | 5000 lines | 2KB | 60% | Low |
| Strict SRP (100 classes) | 30 lines | 120 bytes | 95% | High |
| Moderate SRP (15 classes) | 200 lines | 400 bytes | 85% | Medium |
For most enterprise systems, moderate SRP is the optimal balance. For highload with strict latency budgets — strict SRP can be beneficial (better cache locality), but allocation overhead needs to be compensated with object pooling.
Production Experience
War Story: Billing System Refactoring (2024)
A large telecom company, billing system on Java 17. BillingEngine — 12,000 lines, 67 dependencies, LCOM4 = 8. Four teams worked on it simultaneously. Every release — 3-4 regression bugs, caused by one team breaking another team’s functionality.
Diagnosis:
# SonarQube showed:
# - Cognitive Complexity: 180 (limit 15)
# - LCOM4: 8.0 (norm <= 1)
# - Number of dependencies: 67 (recommendation <= 10)
Refactoring:
BillingEngine (12,000 lines)
├── TariffCalculator (800 lines) — tariff calculation
├── UsageAggregator (600 lines) — consumption aggregation
├── InvoiceGenerator (1,200 lines) — invoice generation
├── DiscountEngine (900 lines) — discounts and bonuses
├── TaxCalculator (500 lines) — taxes
├── PaymentProcessor (700 lines) — payment processing
├── NotificationDispatcher (400 lines) — notifications
└── BillingOrchestrator (300 lines) — coordination
Result:
- LCOM4: 8.0 → 1.0 for each class
- Regression bugs: 3-4 per release → 0-1
- Parallel development: 4 teams without conflicts
- Time to add new tariff type: 2 weeks → 2 days
War Story: Microservices Decomposition (2023)
E-commerce monolith, UserService — 6,000 lines. Authentication team and Profiles team constantly conflicted on merge. LCOM4 = 5, Dependencies = 42.
Split by team responsibility boundaries:
AuthService(authentication, authorization, tokens)ProfileService(user data, preferences)NotificationPreferencesService(notification settings)
Result: merge conflicts dropped by 90%, deployments became independent.
Monitoring and Diagnostics
- ArchUnit: Automatic checks in CI/CD: ```java @ArchTest static final ArchRule classes_should_have_single_responsibility = classes() .that().resideInAPackage(“..domain..”) .should().haveLessThan10Dependencies() .andShould().haveSimpleNameNotContaining(“And”) .andShould().haveSimpleNameNotContaining(“Manager”) .andShould().haveSimpleNameNotContaining(“Processor”) .as(“Domain classes should follow SRP”);
@ArchTest static final ArchRule no_god_classes = noClasses() .that().resideInAPackage(“..service..”) .should().dependOnMoreThan(10Classes()) .as(“No God classes in service layer”); ```
- SonarQube Metrics:
- Cognitive Complexity: >15 → signal of SRP violation
- LCOM4: >1 → class can be split
- Number of dependencies: >10 → potential God Class
- Class size: >500 lines → review for SRP
-
Dependency Graph: JDepend or IntelliJ IDEA Dependency Matrix. If one class connects to everything — it’s a God Class.
- Code Review Rule: If you can’t explain the class’s purpose in 10 seconds — it needs refactoring.
Best Practices for Highload
- Cohesion > Size: Small class ≠ SRP. Large class ≠ SRP violation. Look at cohesion, not size.
- Data Locality: In highload, group by access pattern, not just by domain. Data that’s read together should be together (cache locality).
- Avoid Premature Splitting: Don’t split a class until you see real changes from different stakeholders. The YAGNI principle (“You Aren’t Gonna Need It”) applies to SRP too.
- Use Command/Query Separation (CQS) (separating methods that modify state from those that only read data): Separate classes that change state from classes that only read. This is a natural responsibility split.
Future Trends
- Java 21 Records: Records are natural SRP for data. Their single responsibility is storing immutable data.
equals,hashCode,toString— auto-generated, don’t count as separate responsibilities. - Module System (JPMS): Java 9+ modules help enforce SRP at the package level.
exportsandrequiresformalize responsibility boundaries. - AI-assisted Refactoring: Modern IDEs (IntelliJ IDEA with AI Assistant) suggest automatic extraction of responsibilities from large classes based on field and method usage analysis.
Summary
- SRP is about responsibility boundaries, not class size.
- A class should be a black box for one specific task.
- High internal Cohesion is a sign of correct SRP.
- Trust your intuition: if a class feels “dirty” and “bloated” — it probably is.
- One class — one reason to change.
🎯 Interview Cheat Sheet
Must know:
- 5 SRP tests: Naming (no “and”), Change (who’s the stakeholder?), Dependencies (imports), Mocking (<5), LCOM4
- LCOM4: graph splits into N components → split into N classes; LCOM4 = 1 — single responsibility
- Social factor: “One class — one developer team”; different teams = different responsibilities
- Size doesn’t determine SRP: 30 lines can do 3 things, 300 lines — one complex thing
- Dependency Count > 10+ = potential God Class
- Cross-cutting Concerns (logging, metrics) — solve via AOP/decorators, don’t mix with business logic
- Cohesion > Size: look at cohesion, not size
Frequent follow-up questions:
- How does the JVM “see” SRP violations? — Large object = cache line pollution (200 bytes, 20 used), wasted memory
- What is Anemic Domain Model and SRP? — Only getters/setters — not an SRP violation, but logic moves to God Services
- Is Class Explosion bad? — No, IDEs handle 10,000+ files; fear one 10,000-line file
- Data Locality in highload? — Group by access pattern: data read together should be together
Red flags (DO NOT say):
- “Small class means SRP is followed” (30 lines can do 3 unrelated things)
- “One public method = SRP” (one method can delegate to 10 subsystems)
- “Don’t split until I see problems” (YAGNI is valid, but different stakeholders are already a signal)
Related topics:
- [[1. What is Single Responsibility principle and how to apply it]]
- [[13. How is Single Responsibility related to cohesion]]
- [[14. What happens if a class has multiple reasons to change]]
- [[18. How to refactor God Object]]