The Single Responsibility Principle (SRP)
Among all the SOLID principles, the Single Responsibility Principle is perhaps the least well understood, largely due to its misleading name. Programmers often hear the name and mistakenly assume it means that every module should do just one thing. While there is indeed a principle stating that a function should do one, and only one, thing—a principle applied when refactoring large functions into smaller ones at the lowest levels—this is not the SRP and is not one of the SOLID principles.
Historically, the SRP has been articulated as: a module should have one, and only one, reason to change. Since software systems are modified to satisfy users and stakeholders, these users and stakeholders constitute the “reason to change” referenced in the principle. This understanding allows us to rephrase the principle as: a module should be responsible to one, and only one, user or stakeholder. However, the terms “user” and “stakeholder” are not entirely accurate, as there will likely be multiple users or stakeholders who desire the same type of change. What we are actually referring to is a group—one or more people who require a particular change. We call this group an actor. Thus, the final and most precise version of the SRP states:
A module should be responsible to one, and only one, actor.
The term “module” in this context has a straightforward definition: in most cases, it is simply a source file. However, in languages and development environments that do not use source files to contain code, a module is a cohesive set of functions and data structures. The word “cohesive” is critical here, as cohesion represents the force that binds together code responsible to a single actor, thereby embodying the SRP.
Symptom 1: Accidental Duplication
The best way to understand the SRP is by examining the symptoms that arise from violating it. Consider an Employee class from a payroll application that contains three methods: calculatePay(), reportHours(), and save(). This class violates the SRP because these three methods are responsible to three distinct actors:
- The
calculatePay()method is specified by the accounting department, which reports to the CFO. - The
reportHours()method is specified and used by the human resources department, which reports to the COO. - The
save()method is specified by database administrators (DBAs), who report to the CTO.

By placing the source code for these three methods into a single Employee class, developers have coupled each of these actors to the others, meaning that actions taken by the CFO’s team can affect something the COO’s team depends on.
For example, suppose both calculatePay() and reportHours() share a common algorithm for calculating non-overtime hours. If developers, being careful not to duplicate code, place this algorithm into a function named regularHours(), problems can arise. Imagine the CFO’s team decides that the calculation of non-overtime hours needs adjustment, while the COO’s team in HR does not want this change because they use non-overtime hours for a different purpose. A developer tasked with making the change sees the convenient regularHours() function called by calculatePay() but unfortunately does not notice that it is also called by reportHours(). The developer makes the required change and tests it carefully. The CFO’s team validates that the new function works as desired, and the system is deployed. The COO’s team, unaware of this change, continues to use reports generated by reportHours()—but now these reports contain incorrect numbers. Eventually, the problem is discovered, and the COO is furious because the bad data has cost the budget millions of dollars. Such problems occur because we place code that different actors depend on into close proximity. The SRP says to separate the code that different actors depend on.
Symptom 2: Merges
It is not difficult to imagine that merges will be common in source files that contain many different methods, especially when those methods are responsible to different actors. For instance, suppose the CTO’s team of DBAs decides to implement a simple schema change to the Employee table in the database, while simultaneously the COO’s team of HR clerks decides they need a change in the format of the hours report. Two different developers, possibly from different teams, check out the Employee class and begin making changes. Unfortunately, their changes collide, resulting in a merge. While modern tools are quite capable, no tool can handle every merge case perfectly, and there is always risk involved. In this example, the merge puts both the CTO and the COO at risk, and it is not inconceivable that the CFO could be affected as well. Many other symptoms could be investigated, but they all involve multiple people changing the same source file for different reasons. Once again, the way to avoid this problem is to separate code that supports different actors.
Solutions
There are multiple solutions to this problem, each involving moving the functions into different classes. Perhaps the most obvious approach is to separate the data from the functions. Three classes—PayCalculator, HourReporter, and EmployeeSaver—share access to EmployeeData, which is a simple data structure with no methods. Each class holds only the source code necessary for its particular function, and the three classes are not allowed to know about each other, thus avoiding any accidental duplication. The downside of this solution is that developers must now instantiate and track three separate classes. A common solution to this dilemma is to use the Facade pattern, where an EmployeeFacade class contains very little code and is responsible for instantiating and delegating to the classes with the actual functions.
Some developers prefer to keep the most important business rules closer to the data. This can be accomplished by retaining the most important method in the original Employee class and then using that class as a Facade for the lesser functions. One might object that these solutions result in classes containing just one function, but this is hardly the case. The number of functions required to calculate pay, generate a report, or save data is likely to be substantial in each case, with each class containing many private methods. Each class that contains such a family of methods represents a scope—outside of that scope, no one knows that the private members of the family exist.
The Single Responsibility Principle concerns functions and classes, but it reappears in different forms at higher levels of abstraction. At the component level, it becomes the Common Closure Principle. At the architectural level, it becomes the Axis of Change responsible for the creation of Architectural Boundaries.
The Open-Closed Principle (OCP)
The Open-Closed Principle, coined in 1988 by Bertrand Meyer, states:
A software artifact should be open for extension but closed for modification.
In other words, the behavior of a software artifact ought to be extendible without requiring modification of that artifact. This is the most fundamental reason we study software architecture. If simple extensions to requirements force massive changes to the software, then the architects of that system have engaged in a spectacular failure. Most students of software design recognize the OCP as a principle guiding the design of classes and modules, but the principle takes on even greater significance when considered at the level of architectural components.
A Thought Experiment
Consider a system that displays a financial summary on a web page, where the data is scrollable and negative numbers are rendered in red. Now imagine stakeholders request that this same information be turned into a report for printing on a black-and-white printer. The report should be properly paginated with appropriate page headers, page footers, and column labels, and negative numbers should be surrounded by parentheses. Clearly, some new code must be written, but how much old code will have to change? A good software architecture would reduce the amount of changed code to the barest minimum—ideally, zero.
How can this be achieved?
- By properly separating the things that change for different reasons (the Single Responsibility Principle).
- Then organizing the dependencies between those things properly (the Dependency Inversion Principle).
By applying the SRP, we might arrive at a data-flow view where some analysis procedure inspects the financial data and produces reportable data, which is then formatted appropriately by two reporter processes. The essential insight here is that generating the report involves two separate responsibilities: the calculation of the reported data and the presentation of that data into web- and printer-friendly forms.
Having made this separation, we need to organize the source code dependencies to ensure that changes to one responsibility do not cause changes in the other, and that the behavior can be extended without undue modification. We accomplish this by partitioning the processes into classes and separating those classes into components. In the resulting architecture, we have a Controller component, an Interactor component, a Database component, and four components representing Presenters and Views.

The first critical observation is that all the dependencies are source code dependencies. An arrow pointing from class A to class B means that the source code of class A mentions the name of class B, but class B mentions nothing about class A. Thus, in figure above, FinancialDataMapper knows about FinancialDataGateway through an implements relationship, but FinancialGateway knows nothing at all about FinancialDataMapper. The second observation is that each component boundary is crossed in one direction only, meaning that all component relationships are unidirectional, as shown in the following image. These arrows point toward the components we want to protect from change.

If component A should be protected from changes in component B, then component B should depend on component A. We want to protect the Controller from changes in the Presenters, protect the Presenters from changes in the Views, and protect the Interactor from changes in anything. The Interactor occupies the position that best conforms to the OCP—changes to the Database, Controller, Presenters, or Views will have no impact on the Interactor. Why should the Interactor hold such a privileged position? Because it contains the business rules. The Interactor contains the highest-level policies of the application, while all other components deal with peripheral concerns. The Interactor deals with the central concern.
Even though the Controller is peripheral to the Interactor, it is nevertheless central to the Presenters and Views. While the Presenters might be peripheral to the Controller, they are central to the Views. This creates a hierarchy of protection based on the notion of “level”. Interactors represent the highest-level concept and are therefore the most protected. Views are among the lowest-level concepts and are therefore the least protected. Presenters are higher level than Views but lower level than the Controller or the Interactor. This is how the OCP works at the architectural level: architects separate functionality based on how, why, and when it changes, and then organize that separated functionality into a hierarchy of components, where higher-level components in that hierarchy are protected from changes made to lower-level components.
Directional Control
Much of the complexity in the class design is intended to ensure that dependencies between components point in the correct direction. For example, the FinancialDataGateway interface between the FinancialReportGenerator and the FinancialDataMapper exists to invert the dependency that would otherwise point from the Interactor component to the Database component. The same principle applies to the FinancialReportPresenter interface and the two View interfaces.
Information Hiding
The FinancialReportRequester interface serves a different purpose—it protects the FinancialReportController from knowing too much about the internals of the Interactor. Without this interface, the Controller would have transitive dependencies on the FinancialEntities. Transitive dependencies violate the general principle that software entities should not depend on things they don’t directly use. Even though our first priority is to protect the Interactor from changes to the Controller, we also want to protect the Controller from changes to the Interactor by hiding the Interactor’s internals.
The OCP is one of the driving forces behind the architecture of systems. The goal is to make the system easy to extend without incurring a high impact of change. This goal is accomplished by partitioning the system into components and arranging those components into a dependency hierarchy that protects higher-level components from changes in lower-level components.
The Liskov Substitution Principle (LSP)
In 1988, Barbara Liskov defined subtypes with the following substitution property:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. This idea is known as the Liskov Substitution Principle.
Guiding the Use of Inheritance
Consider a class named License with a method called calcFee(), which is called by the Billing application. There are two subtypes of License: PersonalLicense and BusinessLicense, which use different algorithms to calculate the license fee. This design conforms to the LSP because the behavior of the Billing application does not depend in any way on which of the two subtypes it uses—both subtypes are substitutable for the License type.

The Square/Rectangle Problem
The canonical example of an LSP violation is the infamous square/rectangle problem. In this scenario, Square is not a proper subtype of Rectangle because the height and width of a Rectangle are independently mutable, whereas the height and width of a Square must change together.

Since the user believes it is communicating with a Rectangle, it could easily become confused. Consider the following code:
Rectangle r = ...
r.setW(5);
r.setH(2);
assert(r.area() == 10);
If the code produced a Square, the assertion would fail. The only way to defend against this kind of LSP violation is to add mechanisms to the user (such as an if statement) that detect whether the Rectangle is actually a Square. Since the behavior of the user depends on the types it uses, those types are not substitutable.
LSP and Architecture
In the early years of the object-oriented revolution, we thought of the LSP primarily as a guide for using inheritance. However, over the years, the LSP has evolved into a broader principle of software design that pertains to interfaces and implementations. The interfaces in question can take many forms: a Java-style interface implemented by several classes, several Ruby classes that share the same method signatures, or a set of services that all respond to the same REST interface. In all of these situations, the LSP is applicable because there are users who depend on well-defined interfaces and on the substitutability of the implementations of those interfaces.
The best way to understand the LSP from an architectural viewpoint is to examine what happens to a system’s architecture when the principle is violated. Suppose we are building an aggregator for many taxi dispatch services, where customers use our website to find the most appropriate taxi regardless of the company. Once the customer makes a decision, our system dispatches the chosen taxi using a RESTful service. Assume the URI for the RESTful dispatch service is stored in the driver database. Once our system has chosen an appropriate driver for the customer, it retrieves that URI from the driver record and uses it to dispatch the driver.
Suppose Driver Bob has a dispatch URI like purplecab.com/driver/Bob. Our system appends the dispatch information to this URI and sends it with a PUT request: purplecab.com/driver/Bob/pickupAddress/24 Maple St./pickupTime/153/destination/ORD. This means that all dispatch services for all different companies must conform to the same REST interface, treating the pickupAddress, pickupTime, and destination fields identically.
Now suppose the Acme taxi company hired programmers who did not read the specification carefully and abbreviated the destination field to just dest. Due to business relationships, we cannot simply reject Acme’s service. What would happen to our system’s architecture? We would need to add a special case—the dispatch request for any Acme driver would have to be constructed using a different set of rules from all other drivers. The simplest way to accomplish this would be to add an if statement to the module that constructs the dispatch command: if (driver.getDispatchUri().startsWith("acme.com"))...
However, no architect worth their salt would allow such a construction to exist in the system. Putting the word “acme” directly into the code creates opportunities for all kinds of horrible and mysterious errors, not to mention security breaches. For example, if Acme became even more successful and acquired the Purple Taxi company, maintaining separate brands and websites but unifying systems, would we have to add another if statement for “purple”? The architect would have to insulate the system from such bugs by creating a dispatch command creation module driven by a configuration database keyed by the dispatch URI. The configuration data might specify URI formats for different services. The architect has had to add a significant and complex mechanism to deal with the fact that the interfaces of the RESTful services are not all substitutable.
The LSP can, and should, be extended to the level of architecture. A simple violation of substitutability can cause a system’s architecture to be polluted with a significant amount of extra mechanisms.
The Interface Segregation Principle (ISP)
The Interface Segregation Principle addresses situations where several users utilize the operations of a class. Suppose there are several users who use the operations of an OPS class. Assume that User1 uses only op1, User2 uses only op2, and User3 uses only op3.

If OPS is a class written in a language like Java, the source code of User1 will inadvertently depend on op2 and op3, even though it doesn’t call them. This dependence means that a change to the source code of op2 in OPS will force User1 to be recompiled and redeployed, even though nothing it cared about has actually changed.
This problem can be resolved by segregating the operations into interfaces. If we implement this in a statically typed language like Java, the source code of User1 will depend on U1Ops and op1, but will not depend on OPS. Thus, a change to OPS that User1 does not care about will not cause User1 to be recompiled and redeployed.
ISP and Language
The description of the ISP depends critically on language type. Statically typed languages like Java force programmers to create declarations that users must import, use, or otherwise include. These included declarations in source code create the source code dependencies that force recompilation and redeployment. In dynamically typed languages like Ruby and Python, such declarations do not exist in source code—they are inferred at runtime. Thus, there are no source code dependencies to force recompilation and redeployment. This is the primary reason that dynamically typed languages create systems that are more flexible and less tightly coupled than statically typed languages. This fact might lead one to conclude that the ISP is a language issue rather than an architecture issue.
ISP and Architecture
However, if we examine the root motivations of the ISP, we can see a deeper concern. In general, it is harmful to depend on modules that contain more than you need. This is obviously true for source code dependencies that can force unnecessary recompilation and redeployment, but it is also true at a much higher, architectural level. Consider an architect working on a system S who wants to include a framework F. Suppose the authors of F have bound it to a particular database D, so S depends on F, which depends on D. If D contains features that F does not use and that S does not care about, changes to those features within D may force the redeployment of F and therefore the redeployment of S. Even worse, a failure of one of the features within D may cause failures in F and S.
The lesson here is that depending on something that carries baggage you don’t need can cause troubles you didn’t expect.
The Dependency Inversion Principle (DIP)
The Dependency Inversion Principle tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
In a statically typed language like Java, this means that use, import, and include statements should refer only to source modules containing interfaces, abstract classes, or some other kind of abstract declaration. Nothing concrete should be depended on. The same rule applies for dynamically typed languages like Ruby and Python, where source code dependencies should not refer to concrete modules. In these languages, a concrete module is any module in which the functions being called are implemented.
Clearly, treating this idea as an absolute rule is unrealistic, because software systems must depend on many concrete facilities. For example, the String class in Java is concrete, and it would be unrealistic to force it to be abstract. The source code dependency on the concrete java.lang.String cannot and should not be avoided. However, the String class is very stable—changes to it are very rare and tightly controlled. Programmers and architects do not have to worry about frequent and capricious changes to String. For these reasons, we tend to ignore the stable background of operating system and platform facilities when it comes to DIP. We tolerate those concrete dependencies because we know we can rely on them not to change. It is the volatile concrete elements of our system that we want to avoid depending on—those modules that we are actively developing and that are undergoing frequent change.
Stable Abstractions
Every change to an abstract interface corresponds to a change to its concrete implementations. Conversely, changes to concrete implementations do not always, or even usually, require changes to the interfaces they implement. Therefore, interfaces are less volatile than implementations. Good software designers and architects work hard to reduce the volatility of interfaces by finding ways to add functionality to implementations without making changes to the interfaces. This is Software Design 101.
The implication is that stable software architectures avoid depending on volatile concretions and favor the use of stable abstract interfaces. This principle boils down to a set of very specific coding practices:
- Don’t refer to volatile concrete classes. Refer to abstract interfaces instead. This rule applies in all languages, whether statically or dynamically typed. It also puts severe constraints on the creation of objects and generally enforces the use of Abstract Factories.
- Don’t derive from volatile concrete classes. This is a corollary to the previous rule but bears special mention. In statically typed languages, inheritance is the strongest and most rigid of all source code relationships and should be used with great care. In dynamically typed languages, inheritance is less problematic but is still a dependency, and caution is always wise.
- Don’t override concrete functions. Concrete functions often require source code dependencies. When you override those functions, you do not eliminate those dependencies—you inherit them. To manage those dependencies, you should make the function abstract and create multiple implementations.
- Never mention the name of anything concrete and volatile. This is simply a restatement of the principle itself.
Factories
To comply with these rules, the creation of volatile concrete objects requires special handling because, in virtually all languages, creating an object requires a source code dependency on the concrete definition of that object. In most object-oriented languages like Java, we would use an Abstract Factory to manage this undesirable dependency.
The Application uses ConcreteImpl through the Service interface. However, the Application must somehow create instances of ConcreteImpl. To achieve this without creating a source code dependency on ConcreteImpl, the Application calls the makeSvc method of the ServiceFactory interface. This method is implemented by the ServiceFactoryImpl class, which derives from ServiceFactory. That implementation instantiates the ConcreteImpl and returns it as a Service.

The architectural boundary separates the abstract from the concrete. All source code dependencies cross that boundary pointing in the same direction, toward the abstract side. The boundary divides the system into two components: one abstract and the other concrete. The abstract component contains all the high-level business rules of the application. The concrete component contains all the implementation details that those business rules manipulate. Note that the flow of control crosses the boundary in the opposite direction of the source code dependencies. The source code dependencies are inverted against the flow of control, which is why we refer to this principle as Dependency Inversion.
Concrete Components
The concrete component contains a single dependency, so it violates the DIP. This is typical—DIP violations cannot be entirely removed, but they can be gathered into a small number of concrete components and kept separate from the rest of the system. Most systems will contain at least one such concrete component, often called main because it contains the main function. In the illustrated case, the main function would instantiate the ServiceFactoryImpl and place that instance in a global variable of type ServiceFactory. The Application would then access the factory through that global variable.
As we progress to higher-level architectural principles, the DIP will show up again and again. It will be the most visible organizing principle in architecture diagrams. The architectural boundary will become a central feature in later chapters, and the way dependencies cross that boundary in one direction, toward more abstract entities, will become a new rule called the Dependency Rule.