What is the single responsibility principle?

The single responsibility principle (SRP) says a software component should have one reason to change.

That definition is more precise than simply saying a class should "do one thing." A class can contain several closely related methods and still satisfy SRP if those methods all support the same responsibility and change for the same reason.

When unrelated responsibilities accumulate in one class, changes to one concern can unexpectedly affect another. SRP encourages developers to separate those concerns into focused components.

Single responsibility and SOLID

SRP is the first of the five SOLID object-oriented design principles:

  • Single responsibility principle.
  • Open/closed principle.
  • Liskov substitution principle.
  • Interface segregation principle.
  • Dependency inversion principle.

Benefits of the single responsibility principle

Well-designed SRP components tend to provide several practical benefits:

  • Simplicity. Focused classes are easier to understand.
  • Testability. Tests can target one responsibility with fewer unrelated dependencies.
  • Maintainability. Changes are easier to isolate.
  • Reusability. Focused components are easier to reuse in other contexts.
  • Collaboration. Developers are less likely to edit the same large class simultaneously.
  • Refactoring. Clearly separated responsibilities are easier to reorganize as a system evolves.

SRP does not automatically make software scalable or fast. Its primary value is structural: it helps developers keep responsibilities and reasons for change separate.

A Java 15 single responsibility example

Imagine a number guessing game that records the magic number and how many guesses the player required.

Java 15 supports records as a preview feature. A record is a concise way to model immutable data, which makes it a useful fit for a game result:

public record GameResult(int guesses, int magicNumber) {}

The responsibility of GameResult is clear: represent the result of one game. It does not manage a collection of previous games, print reports or modify application-wide history.

If you prefer to avoid Java 15 preview features, the same design can use an ordinary final class:

public final class GameResult {
    private final int guesses;
    private final int magicNumber;

    public GameResult(int guesses, int magicNumber) {
        this.guesses = guesses;
        this.magicNumber = magicNumber;
    }

    public int guesses() {
        return guesses;
    }

    public int magicNumber() {
        return magicNumber;
    }
}

An SRP violation in Java

Suppose the application now needs to retain every result. It might be tempting to put a static history collection directly inside GameResult:

public class GameResult {
    private static final List<GameResult> history = new ArrayList<>();

    private final int guesses;
    private final int magicNumber;

    public GameResult(int guesses, int magicNumber) {
        this.guesses = guesses;
        this.magicNumber = magicNumber;
        history.add(this);
    }

    public static void printHistory() {
        // Reporting logic
    }

    public static void deleteHistory() {
        // History-management logic
    }
}

The class now has multiple reasons to change. Changes to the representation of a game result affect the same component that owns history storage and reporting behavior.

It also introduces hidden global state. Simply constructing a GameResult unexpectedly modifies a static collection, which makes the class harder to reason about and test.

Refactor the SRP violation

A cleaner design keeps GameResult focused on result data and moves collection management into a separate class:

public final class GameHistory {
    private final List<GameResult> results = new ArrayList<>();

    public void add(GameResult result) {
        results.add(result);
    }

    public List<GameResult> results() {
        return List.copyOf(results);
    }

    public void clear() {
        results.clear();
    }
}

This design separates two responsibilities:

  • GameResult represents one completed game.
  • GameHistory manages a collection of game results.

The history is also instance state rather than static global state. That makes separate histories easy to create and makes unit tests much easier to isolate.

Use the refactored Java classes

The calling code remains straightforward:

var history = new GameHistory();

history.add(new GameResult(4, 7));
history.add(new GameResult(2, 3));

history.results().forEach(System.out::println);

With the Java 15 record version, the generated toString() method also produces useful output automatically.

Should printing belong in GameHistory?

As the application grows, SRP can be applied again. If history storage and history presentation begin changing for different reasons, move presentation into another component:

public final class GameHistoryPrinter {
    public void print(GameHistory history) {
        history.results().forEach(System.out::println);
    }
}

But don't split classes merely because you can. If a small application has a simple print() method that naturally belongs with its history abstraction, another class may add more complexity than value.

Single responsibility does not mean one method per class

A common misunderstanding of SRP is that every class should contain only one method or perform only one tiny operation.

That approach often creates needless fragmentation. A class can have many methods while still having one responsibility.

For example, these methods could reasonably belong to the same history component:

public void add(GameResult result) { ... }
public void remove(GameResult result) { ... }
public List<GameResult> results() { ... }
public void clear() { ... }

All four operations manage game history. They belong to the same cohesive responsibility.

Drawbacks of applying SRP too aggressively

The single responsibility principle is a design guideline, not a command to create the smallest classes possible.

Excessive decomposition can:

  • create too many tiny classes;
  • make simple workflows difficult to follow;
  • increase navigation across the codebase;
  • introduce unnecessary abstractions and dependencies; and
  • make an application harder rather than easier to maintain.

The goal is high cohesion: keep behavior that changes for the same reason together and separate behavior that changes for different reasons.

Java single responsibility principle best practices

When reviewing a Java class for SRP, ask these questions:

  • What is this class responsible for?
  • How many distinct reasons could cause this class to change?
  • Does constructing the object produce unrelated side effects?
  • Does the class contain unrelated persistence, presentation or business logic?
  • Can the class be tested without configuring unrelated dependencies?
  • Would splitting a responsibility actually make the design easier to understand?

Apply SRP where it clarifies boundaries and isolates change. Avoid turning it into a mechanical rule that creates abstractions without a concrete design benefit.

The SOLID principles of object-oriented programming.
Single responsibility is the first of the five SOLID object-oriented design principles.