Understanding the Single Responsibility Principle
SOLID represents a collection of design guidelines that help developers build code that scales gracefully, adapts to new requirements, and simplifies swapping out existing implementations. Even though these ideas have been around for decades, they remain highly relevant and form a cornerstone of modern object-oriented development. As an Angular developer, integrating these principles into your daily routine is well worth the effort.
Here’s what the SOLID acronym breaks down into:
- Single Responsibility Principle,
- Open/Closed Principle,
- Liskov Substitution Principle,
- Interface Segregation Principle,
- Dependency Inversion Principle.
The Single Responsibility Principle Explained
Let’s start with the first letter of the acronym, S, which stands for the Single Responsibility Principle.

Credit: Derick Bailey, CC-SA 3.0
The graphic above illustrates a common pitfall: your code, whether it’s a class or a method, can end up acting like a Swiss Army knife, capable of handling a wide range of tasks. Just because you *can* write code that way doesn’t mean you *should*.
“A class should have one, and only one, reason to change”.
To put it in Robert Martin’s terms, when you’re writing code, you should structure it so that a class has exactly one trigger for modification when business requirements evolve.
Why bother following this rule? The advantages are clear:
- Writing code becomes easier since each file focuses on a narrower set of functionality rather than juggling multiple concerns.
- It helps prevent unexpected “side-effects” from sneaking into your logic.
- It minimizes the number of classes that need to be touched. The more responsibilities a class carries, the more often it will require changes. The goal is to keep modifications to a minimum, since every alteration can ripple out to affect dependent classes or components.
When classes and objects stick to a single job, they tend to be:
- much simpler to grasp,
- less prone to bugs,
- quicker to build upon, which speeds up the overall development workflow.
How can you make sure you’re adhering to this principle? A simple habit helps: before you change existing code or write something new, pause and ask yourself what responsibility this piece of code actually owns. If you find yourself using the word “and” to list multiple duties, that’s a strong signal it’s time to consider refactoring.
Take a look at the following illustration:

This snippet comes from a component responsible for rendering a folder tree. Notice the different methods it contains:
- calculateIndent → this method determines the indentation level for a folder within the tree, based on its nesting depth.
- expand → this one handles opening a folder to reveal its subfolders.
- getColorByStatus → this calculates the folder’s color, which depends on its current status, like whether it’s archived.
There’s clearly too much happening in one place. The component is trying to handle all these calculations internally because it doesn’t have a clear owner to delegate to. As a result, the logic is tough to follow, cluttered with various implementations, and difficult to write unit tests for.
A cleaner approach is to move these methods into dedicated services.

After this refactoring, you end up with services like these:
- FolderIndentService – takes over the responsibility of the calculateIndent method, computing the correct indentation for any folder based on its position in the tree.
- FolderToggleService – assumes the role of the expand method, managing the open and close behavior for folders.
- FolderColorService – inherits the getColorByStatus logic, determining the appropriate color for a folder based on its status.
With this breakdown, the component no longer needs to know how to perform these tasks. It simply knows which service to call for each action. This separation makes unit testing more straightforward, as you can now test individual services in isolation. Plus, these services can be reused elsewhere in the application, which helps cut down on duplicated code.
Let’s examine another scenario.
Suppose you have a dialog used to create a new folder. It includes a form and a confirmation button, and it also surfaces validation errors, like when the “name” field is left empty. Now, imagine you decide to add folder editing functionality to this same component. Suddenly, that one component is handling:
- folder creation,
- folder editing,
- the form controls,
- data validation.
Once again, this is a case of overloading one component with too many jobs. A better solution would be to split it into three distinct components, as shown in this diagram:

- FolderFormComponent – its only job is to host the form and handle its validation.
- FolderEditDialog – this wraps the FolderFormComponent and adds the editing logic, such as sending an update request to the backend.
- FolderCreateDialog – this also wraps the FolderFormComponent but only incorporates the creation logic.
By doing this, you’ve successfully separated the following concerns:
- creation,
- editing,
- presentation and data validation.
This is a practical application of the Single Responsibility Principle in action.
In the upcoming article, we’ll dive into the Open/Closed Principle. See you there 🙂
