You are currently viewing Single Responsibility Principle in Java

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

Noel Kamphoa

Experienced software engineer with expertise in Telecom, Payroll, and Banking. Now Senior Software Engineer at Societe Generale Paris.