Single Responsibility Principle in Java
## Introduction
The **Single Responsibility Principle (SRP)** is the “S” in the SOLID principles of object-oriented design. It’s often mentioned, sometimes misunderstood, and rarely applied correctly in large Java applications.
> **Definition** : A class should have only one reason to change.
In practice, this means a class should encapsulate **a single, well-defined responsibility** — not “one method” or “one functionality,” but **one axis of change**.
In this article, we'll go beyond trivial examples and explore **real-world SRP violations** , how to spot them, and how to refactor for maintainability and testability.
## 1. A Real-World Example: User Registration
Consider the following `UserService` class:
public class UserService {
public void register(String email, String password) {
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
String hashed = BCrypt.hash(password);
User user = new User(email, hashed);
userRepository.save(user);
sendWelcomeEmail(user);
}
private void sendWelcomeEmail(User user) {
// SMTP connection setup
// Template rendering
// Email dispatch
}
}
This looks fine... until you need to:
* Add a new hashing algorithm
* Change email provider
* Handle registration for third-party OAuth
SRP is violated. Why?
## 2. Identifying the Responsibilities
The `UserService` class currently does **at least three things** :
1. **Validates input**
2. **Manages user persistence**
3. **Handles email communication**
Each of these concerns could change independently.
* Marketing wants to change email templates.
* Security wants a new hashing policy.
* DevOps wants to decouple SMTP config.
All are _reasons to change_. That’s your SRP alarm.
## 3. Refactoring for SRP
Let’s extract each responsibility into its own class.
### ✅ Extract validation:
public class RegistrationValidator {
public void validate(String email) {
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
}
### ✅ Extract password logic:
public class PasswordEncoder {
public String encode(String password) {
return BCrypt.hash(password);
}
}
### ✅ Extract email logic:
public class WelcomeMailer {
private final EmailClient client;
public WelcomeMailer(EmailClient client) {
this.client = client;
}
public void send(User user) {
client.send(user.getEmail(), "Welcome", "Thanks for joining!");
}
}
### 🔁 Updated `UserService`:
public class UserService {
private final UserRepository userRepository;
private final RegistrationValidator validator;
private final PasswordEncoder encoder;
private final WelcomeMailer mailer;
public UserService(UserRepository userRepository,
RegistrationValidator validator,
PasswordEncoder encoder,
WelcomeMailer mailer) {
this.userRepository = userRepository;
this.validator = validator;
this.encoder = encoder;
this.mailer = mailer;
}
public void register(String email, String password) {
validator.validate(email);
String hashed = encoder.encode(password);
User user = new User(email, hashed);
userRepository.save(user);
mailer.send(user);
}
}
Now:
* Each class has **one reason to change**
* You can **test independently**
* Replacing email providers or validators becomes trivial
## 4. When Is SRP Worth It?
Over-applying SRP can lead to fragmentation in **small projects**. But in **medium to large systems** , SRP is essential for:
* Isolated unit testing
* Team collaboration
* Clean domain boundaries
A good rule: **When a class grows past 50–70 lines and touches multiple infrastructure layers, SRP may be at risk.**
## 4.5. SRP vs. Microservices: A Note
A common misunderstanding is to equate the **Single Responsibility Principle (SRP)** with microservices — as if one microservice should only do one thing. But **SRP applies at the class level** , not the system level.
In fact, it’s possible (and common) to:
* Have a **monolith** that applies SRP cleanly within each service or module.
* Build a **microservice** that violates SRP internally by mixing responsibilities into one giant class (e.g., a `UserService` that also sends emails, logs metrics, and transforms DTOs).
SRP helps define **cohesion within a module or microservice** , not how many services you should have.
### ✅ Example
Even in a microservice like `OrderService`, you can still break SRP:
class OrderService {
void createOrder(...) { ... }
void sendConfirmationEmail(...) { ... }
void updateInventory(...) { ... }
}
This service may be “small” and “independent,” but the class itself handles **too many responsibilities**.
The SRP-compliant version would delegate:
* `OrderProcessor` → handles core business logic
* `InventoryUpdater` → manages stock
* `NotificationService` → sends emails
SRP helps you **organize code within microservices** , not decide how many services to build.
## Conclusion
The Single Responsibility Principle helps you write Java code that is **easier to change** , **easier to test** , and **easier to understand**.
In the real world, SRP is less about counting methods and more about **clarifying responsibilities**. When applied well, it becomes a foundation for clean, robust architecture.
You can find the complete code of this article here in GitHub.
> 📚 Related: Open/Closed Principle in Java
Originally published on my blog: https://nkamphoa.com/single-responsibility-principle-in-java/