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:
- Cooking meals in the kitchen.
- Delivering take-out orders across town on a scooter.
- Managing payroll and filing corporate tax returns.
- 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:
- Relational Database Schema (Domain/Data model): Driven by SQL normalization, indexing, and DB migrations.
- 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
passwordHashorssnto the Entity automatically exposes it in the JSON response if an engineer forgets@JsonIgnore. - Circular Serialization: Bidirectional JPA relationships (
@ManyToOneand@OneToMany) trigger aStackOverflowErrorduring 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:
- Execute the legacy class against a large, representative sample of production-like inputs (e.g., 1,000 diverse requests).
- Capture and record all outputs, database state changes, and generated files into an immutable reference baseline (Golden Master).
- Create an automated test that asserts future refactored code produces the exact byte-for-byte result as the Golden Master.
- 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()andsendEmail()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),
@Entityclasses 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
@Transactionaldatabase 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
@Entityand 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
@Transactionalmethod 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
BigDecimalorStringBuilder, SRP is fully honored).