📐 Section 18 · Question #2

Give an Example of Single Responsibility Principle Violation

A Single Responsibility Principle (SRP) violation occurs when a single class takes on multiple unrelated responsibilities, causing it to change for different, unrelated business...


🟢 Junior Level

A Single Responsibility Principle (SRP) violation occurs when a single class takes on multiple unrelated responsibilities, causing it to change for different, unrelated business reasons. Such a class is commonly known as a God Object or a Swiss Army Knife anti-pattern.

SRP Violator (God Object / Swiss Army Knife):
+--------------------------------------------------------+
|                   OrderController                      |
| + parseAndValidateJson()  --> HTTP / Transport concern |
| + calculateDiscounts()    --> Sales domain logic       |
| + executeSqlInsert()      --> Database / SQL concern   |
| + sendSmsViaTwilio()      --> SMS gateway integration  |
+--------------------------------------------------------+
  (4 distinct architectural layers crammed into one class!)

Real-World Analogy

Imagine the head chef of a restaurant who insists on:

  1. Cooking meals in the kitchen.
  2. Delivering take-out orders across town on a scooter.
  3. Managing payroll and filing corporate tax returns.
  4. Mopping the dining room floors at night.

If the chef gets caught in a traffic jam during a delivery, the kitchen halts, patrons leave hungry, and accounting deadlines are missed. In a well-structured restaurant, each person has a dedicated responsibility: the chef cooks, the courier delivers, and the accountant manages taxes.

Classic Java Anti-Pattern

// ❌ BAD: One class manages calculations, database persistence, HTML rendering, and SMTP
public class Invoice {
    private Long id;
    private BigDecimal amount;

    // 1. Business Logic: Tax calculation
    public BigDecimal calculateTax() {
        return amount.multiply(new BigDecimal("0.20"));
    }

    // 2. Persistence Logic: Direct JDBC / SQL execution
    public void saveToDatabase(DataSource dataSource) throws SQLException {
        String sql = "INSERT INTO invoices (id, amount) VALUES (?, ?)";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setLong(1, id);
            stmt.setBigDecimal(2, amount);
            stmt.executeUpdate();
        }
    }

    // 3. Presentation Logic: HTML report formatting
    public String generateHtmlReport() {
        return "<html><body><h1>Invoice #" + id + "</h1></body></html>";
    }

    // 4. Communication Logic: Direct SMTP dispatch
    public void emailToCustomer(String email, Session mailSession) throws MessagingException {
        MimeMessage message = new MimeMessage(mailSession);
        message.setRecipient(Message.RecipientType.TO, new InternetAddress(email));
        message.setSubject("Invoice #" + id);
        message.setContent(generateHtmlReport(), "text/html");
        Transport.send(message);
    }
}

🟡 Middle Level

1. Real-World Violations in Spring Boot

Violation 1: The “Kitchen Sink” @Service

The most common enterprise anti-pattern is an overloaded service injecting a dozen unrelated components:

// ❌ BAD: GeneralUserService changes whenever ANY department changes requirements
@Service
@RequiredArgsConstructor
public class GeneralUserService {

    private final UserRepository userRepo;
    private final OrderRepository orderRepo;
    private final EmailClient emailClient;
    private final PasswordEncoder encoder;
    private final S3Client s3Client;
    private final AuditLogger auditLogger;

    // Reason 1: Security team updates password complexity rules
    public void registerUser(UserDto dto) { /* ... */ }

    // Reason 2: Cloud team migrates avatar storage from AWS S3 to Google Cloud Storage
    public void uploadAvatar(Long userId, byte[] image) { /* ... */ }

    // Reason 3: Marketing team rewrites the password reset email template
    public void sendPasswordResetEmail(String email) { /* ... */ }

    // Reason 4: Finance team requests a new Excel layout for order exports
    public byte[] exportOrderHistoryToExcel(Long userId) { /* ... */ }
}

Violation 2: Merging JPA @Entity with REST DTO

// ❌ BAD: Database entity directly exposed as JSON payload
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "password_hash")
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // REST Jackson detail!
    private String password;

    @Transient
    @Pattern(regexp = "^\\+?[0-9]{10,13}$") // HTTP Request validation detail!
    private String rawPhoneInput;
}

Why this fails: A frontend change to the JSON schema risks corrupting the relational database table schema, and vice versa.

2. Refactoring via Clean Architecture

// ✅ 1. Pure Domain Model (No frameworks, only domain attributes)
public record User(Long id, String username, String email, String passwordHash) {}

// ✅ 2. Focused Validator Component
@Component
public class UserRegistrationValidator {
    public void validate(RegistrationRequest request) {
        if (request.username() == null || request.username().isBlank()) {
            throw new IllegalArgumentException("Username cannot be blank");
        }
    }
}

// ✅ 3. Focused Password Security Service
@Service
@RequiredArgsConstructor
public class PasswordService {
    private final PasswordEncoder passwordEncoder;

    public String hashPassword(String rawPassword) {
        return passwordEncoder.encode(rawPassword);
    }
}

// ✅ 4. Focused Persistence Interface
public interface UserRepository {
    User save(User user);
    Optional<User> findByEmail(String email);
}

// ✅ 5. Focused Notification Service
@Service
@RequiredArgsConstructor
public class UserNotificationService {
    private final MailSender mailSender;

    public void sendWelcomeEmail(User user) {
        // Asynchronous email dispatch
    }
}

// ✅ 6. Orchestrator (Single Responsibility: Coordinating the registration flow)
@Service
@RequiredArgsConstructor
public class UserRegistrationCoordinator {

    private final UserRegistrationValidator validator;
    private final PasswordService passwordService;
    private final UserRepository userRepository;
    private final UserNotificationService notificationService;

    @Transactional
    public User register(RegistrationRequest request) {
        validator.validate(request);
        String hash = passwordService.hashPassword(request.password());
        User newUser = new User(null, request.username(), request.email(), hash);
        User savedUser = userRepository.save(newUser);
        notificationService.sendWelcomeEmail(savedUser);
        return savedUser;
    }
}

🔴 Senior Level

1. Destructive Production Consequences of SRP Violations

Consequence Root Cause Production Manifestation
Code Fragility Unrelated logic sharing private state. Fixing an email formatting bug inadvertently breaks order discount calculations.
Code Immobility High fan-out coupling to unrelated subsystems. Cannot reuse an order tax calculation routine in another service without dragging in PDF and SMTP libraries.
Git Merge Conflicts Multiple developers editing the same class. 5 engineers working on auth, billing, avatars, and reports constantly collide in merge conflicts.
Connection Pool Starvation Mixing persistence with slow network I/O in transactions. Database pool is exhausted under moderate load, freezing the entire application.

2. HikariCP Connection Pool Starvation Case Study

When an SRP-violating method mixes database persistence with external network I/O inside a @Transactional boundary, critical infrastructure failure occurs:

// ❌ DISASTROUS: Transaction holds HikariCP connection during remote HTTP call
@Service
@RequiredArgsConstructor
public class FragileOrderService {

    private final OrderRepository orderRepository;
    private final ExternalPaymentClient paymentClient;

    @Transactional // 1. HikariCP checks out an active JDBC Connection!
    public void checkout(OrderDto dto) {
        Order order = orderRepository.save(new Order(dto)); // 2. Fast DB INSERT (1-2 ms)

        // 3. SLOW NETWORK CALL while holding the JDBC connection!
        // If the bank takes 3-5 seconds to respond, this DB connection is blocked!
        paymentClient.charge(dto.getPaymentInfo());

        order.markPaid();
        orderRepository.save(order); // 4. COMMIT and connection release
    }
}
HikariCP Connection Pool Exhaustion:
Pool Size: 30 Connections
Concurrent Requests: 100
HTTP Gateway Latency: 3,000 ms

30 Threads grab 30 JDBC connections --> Wait for slow HTTP responses.
70 Threads queue up waiting for an available DB connection --> TimeoutException!
Result: Complete application outage for ALL database operations!

Architectural Fix: Separate persistence from remote I/O. Perform the remote HTTP call outside the database transaction boundary, opening the @Transactional boundary only for the local state updates.

3. Adding Cross-Cutting Concerns via Decorators

Instead of bloating services with metrics, caching, and audit logging:

// 1. Domain Contract
public interface OrderService {
    Order createOrder(OrderRequest request);
}

// 2. Core Business Logic (SRP: Only creation rules)
@Service
@Primary
public class CoreOrderService implements OrderService {
    @Override
    public Order createOrder(OrderRequest request) {
        return new Order(request);
    }
}

// 3. Decorator for Metrics (SRP: Only telemetry instrumentation)
@Service
@RequiredArgsConstructor
public class TimedOrderServiceDecorator implements OrderService {

    private final OrderService delegate;
    private final MeterRegistry meterRegistry;

    @Override
    public Order createOrder(OrderRequest request) {
        return meterRegistry.timer("orders.creation")
            .record(() -> delegate.createOrder(request));
    }
}

4 Tricky Questions

1. How does combining @Entity (JPA) and @JsonProperty (Jackson) violate SRP and what production risks does it create?

Answer: The class combines two conflicting concerns:

  1. Relational Database Schema (Domain/Data model): Driven by SQL normalization, indexing, and DB migrations.
  2. Public REST API Contract (Transport model): Driven by frontend screens, mobile clients, and backwards compatibility.

Production Risks:

  • Data Leaks (Over-fetching): Adding a sensitive field like passwordHash or ssn to the Entity automatically exposes it in the JSON response if an engineer forgets @JsonIgnore.
  • Circular Serialization: Bidirectional JPA relationships (@ManyToOne and @OneToMany) trigger a StackOverflowError during Jackson serialization.
  • Breaking API Changes: Refactoring a database column name breaks downstream mobile clients if not caught.

Fix: Maintain strict separation between OrderEntity and OrderResponseDto, mapping between them using MapStruct.

2. What are Characterization Tests (Golden Master Pattern) and how are they used when dismantling a legacy God Object?

Answer: When refactoring a 5,000-line legacy God Class, unit tests are rarely present, and the code contains hidden side effects that make writing mock-heavy unit tests almost impossible.

The Golden Master Protocol:

  1. Execute the legacy class against a large, representative sample of production-like inputs (e.g., 1,000 diverse requests).
  2. Capture and record all outputs, database state changes, and generated files into an immutable reference baseline (Golden Master).
  3. Create an automated test that asserts future refactored code produces the exact byte-for-byte result as the Golden Master.
  4. Safely extract methods into focused SRP classes under the safety net of this regression suite.

3. Why is CQRS (Command Query Responsibility Segregation) a direct architectural manifestation of SRP?

Answer: Writes (Commands) and Reads (Queries) change for fundamentally different business reasons:

  • Commands: Require strict domain invariants, optimistic locking, relational normalization, and ACID transactions. Driven by domain business rules.
  • Queries: Require denormalized projections, caching, full-text search, and pagination. Driven by UI/UX screen requirements.

Combining both in a single service forces that service to change whether the business changes a payroll calculation or the UI designer adds a sorting filter to a web table. Splitting into OrderCommandService and OrderQueryService gives each service a single reason to change.

4. How do you distinguish between Cohesion and Coupling using a class that violates SRP?

Answer:

  • Cohesion (Internal Focus): Measures how closely the internal elements of a class work together toward a single goal. An SRP violator has Extremely Low Cohesion: methods like calculateTax() and sendEmail() do not share private fields or algorithmic logic.
  • Coupling (External Dependency): Measures how tightly bound a class is to other classes. An SRP violator has Extremely High Coupling: it depends on dozens of third-party SDKs, databases, template engines, and email protocols, meaning any external change risks breaking the class.

🎯 Interview Cheat Sheet

Core Concepts:

  • SRP Violation Definition: A class having multiple reasons to change (serving multiple independent business actors).
  • Classic Offenders: God Objects (3,000+ line services), @Entity classes decorated with JSON annotations, and methods mixing JDBC with remote HTTP calls.
  • Severe Production Risk: Connection Pool Starvation occurs when long-running HTTP calls are wrapped inside @Transactional database boundaries.
  • Cross-Cutting Concerns: Implement caching, logging, and metrics using the Decorator Pattern or Spring AOP, rather than polluting domain services.
  • Clean Architecture: Separate persistence (OrderEntity), transport (OrderDto), and orchestration (OrderCoordinator).

Red Flags to Avoid:

  • ❌ “Combining @Entity and REST DTOs is fine because it saves boilerplate code.” (Creates security data leaks, circular serialization errors, and API fragility).
  • ❌ “Calling external banking or SMS APIs inside a Spring @Transactional method is safe.” (Exhausts the database connection pool under load).
  • ❌ “A class with 20 methods always violates SRP.” (If all 20 methods operate on common state for a single purpose, like BigDecimal or StringBuilder, SRP is fully honored).