Question 21 · Section 18

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.

Language versions: English Russian Ukrainian

🟢 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

  1. 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.

  2. 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.

  3. 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

  1. Anemic Domain Model: User and Order with 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.

  2. 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.

  3. God Class with a “Good” Name: PaymentOrchestrator at 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.

  4. 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.
  • 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. exports and requires formalize 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]]