Software systems rarely fail because of a single large mistake. More often, they fail because small, seemingly reasonable changes interact in unexpected ways.
The single responsibility principle exists to prevent exactly this kind of failure by defining clear ownership boundaries within code. Rather than focusing on classes or functions in isolation, this principle emphasizes who is allowed to change a module—and why. Understanding this people-centric perspective is key to applying the principle effectively in real-world systems, especially when multiple teams or stakeholders are involved.
When a Class Has Multiple Responsibilities
Let’s illustrate this principle using an example, the Product class, as shown in the figure below. This class includes the CalculateTax and GetHTMLRepresentation methods as well as the WebStore and Accounting actors.

Multiple Actors Sharing Responsibility
The CalculateTax method was defined by the Accounting actor and is only called by the latter. The WebStore actor is responsible for the specification and implementation of the GetHTMLRepresentation method and has expanded the Product class accordingly.
With its structure, the Product class violates the single responsibility principle since multiple actors share responsibility for the class and can initiate adjustments. Combining the source code of multiple actors into one class results in a tight coupling between actors that are otherwise meant to be independent.
If changes are later required for the WebShop, errors may arise in the logic of another actor due to possibly sharing existing source code.
Shared Logic and Tight Coupling
In this example, the inner private calculatePrice method determines a price from both methods, as shown in the listing below. In the HTML interface of the store, the price is displayed next to the tax rate, and the accounting team uses the value as the basis for calculating the overall price.
type Product struct {
Name string
PriceInCent int
TaxRatePercentage int
}
func (p *Product) calculatePriceInCent() int {
// extensive logic!
return p.PriceInCent
}
func (p *Product) CalculateTax() int {
return p.calculatePriceInCent()/100 + p.TaxRatePercentage*p.TaxRatePercentage
}
func (p *Product) GetHTMLRepresentation() string {
return fmt.Sprintf("<div>%d cent plus %d %% VAT</div>",
p.calculatePriceInCent(), p.TaxRatePercentage)
}
How Single Responsibility Principle Violations Lead to Production Errors
By expanding the store interface, only the final price will be presented in the user interface with immediate effect. The web store developers then decided to use calculate-Price to determine the overall price for end customers instead of the net price and to display this directly.
When One Team’s Change Breaks Another Team’s Logic
The tests carried out by the store team are successful. However, the developers did not realize that the adjustment also had an impact on the accounting team. In its logic, value-added tax is added twice in the tax calculation because the tax is calculated on the basis of the calculatePrice method, which no longer provides a net price. At this stage, deploying the application without further testing would be fatal.
Parallel Development and Merge Conflicts
The more code is managed by several teams together, the more likely that changes made by one team will impact another. Even if the changes made by the two teams affect different functionalities, several teams might be working in parallel on the same source code file. Then, conflicts may arise in the version management system during the merging of the various changes. If these conflicts are not cleared up correctly, even more errors will occur.
Applying the Single Responsibility Principle
The single responsibility principle can help solve this problem. Several implementations are possible that differ only in the distribution of the data and the functionality of the classes concerned. In all variants, however, the logic required by each actor is extracted into separate components and can be maintained there separately.
Complete Separation of Data and Functionality
This figure shows an implementation in which the data (i.e., the attributes) of the Product class are completely separated from its functionality.

The logic of each actor is implemented in a separate class, and both implementations access the ProductData class, which acts as a data container.
Each actor is responsible for its own class, and changes to any actor only have a local effect on their own subarea.
One disadvantage of implementing this solution is that the web store developers and the accounting developers must each know their corresponding data and logic classes. In our example, the WebStore must explicitly know, instantiate, and use the HTMLRender and ProductData classes.
Separation of Functionalities and the Use of a Facade
As an alternative to a complete separation and disclosure of the details of the Product class, what’s called a facade can be included, as shown in the next figure. This element contains code for instantiating the data and actor logic classes as well as for redirecting the calls to the corresponding methods in the logic classes. With this approach, the data is still stored in the separate ProductData class, but this class no longer needs to be known to the caller.

One advantage of this approach is the simpler interface for the WebStore and Accounting classes, whose responsible developers now don’t need to deal with the details of instantiating or using the data and logic classes. For these developers, the interface remains easy to use.
Using the Data Object as a Façade
A third implementation variant is to use the data object itself as a facade and thus also create the possibility of implementing important functionality within this class. This figure shows a corresponding class diagram.

With this approach, the WebStore and Accounting classes can continue to work with a simple interface, and changes to the extracted logics of the individual actors do not affect each other.
Advantages of the Single Responsibility Principle
For each implementation variant, the logic is implemented in a separate module or class, thus encapsulating the functionality accordingly. This encapsulation improves the readability and maintainability of the resulting code. Errors within the logic classes, for example, can be identified more easily and rectified accordingly.
If functionality is clearly separated and a clear responsibility is defined, it is possible to reuse a logic class more easily in other application areas.
Conclusion
The single responsibility principle is not merely a guideline for cleaner class design—it is a safeguard against organizational and technical friction. By aligning code structure with clear ownership boundaries, the principle reduces the risk that one team’s changes will unintentionally disrupt another’s work. Whether implemented through full separation, facades, or carefully designed data objects, the principle consistently leads to systems that are easier to maintain, safer to change, and more resilient as teams and requirements evolve. In practice, the single responsibility principle succeeds because it acknowledges a fundamental reality of software development: code changes are driven by people, and people need clear boundaries to work effectively.
Editor’s note: This post has been adapted from a section of the book Software Architecture and Design: The Practical Guide to Design Patterns by Kristian Köhler. Kristian is a software architect and developer with a passion for solving problems using efficient, well-structured software. He is the managing director of Source Fellows GmbH.
This post was originally published 2/2026.
Comments