SOLID design principles: Building stable and flexible systems 
Tags: java, beginners, designpatterns, architecture
Published: 2019-02-20 18:42:52

---
title: SOLID design principles: Building stable and flexible systems 
published: true
description: SOLID is one of the most well-known sets of software design principles. It can help you avoid common pitfalls and think about your apps’ architecture from a higher level.
tags: java, beginners, designpatterns, architecture
---

To build stable and flexible software, we need to keep software design principles in mind. Having error-free code is essential. However, well-designed software architecture is just as important.

SOLID is one of the most well-known sets of software design principles. It can help you avoid common pitfalls and think about your apps’ architecture from a higher level.

## What are SOLID design principles?

SOLID design principles are five software design principles that enable you to write effective object-oriented code. Knowing about [OOP principles](https://raygun.com/blog/oop-concepts-java/) like abstraction, encapsulation, inheritance, and polymorphism is important, but how would you use them in your everyday work? SOLID design principles have become so popular in recent years because they answer this question in a straightforward way.

The SOLID name is a mnemonic acronym where each letter represents a software design principle, as follows:

1.  S for Single Responsibility Principle
2.  O for Open/Closed Principle
3.  L for Liskov Substitution Principle
4.  I for Interface Segregation Principle
5.  D for Dependency Inversion Principle

The five principles overlap here and there, and programmers use them broadly. SOLID principles lead to more flexible and stable software architecture that’s easier to maintain and extend, and less likely to break.

## Single Responsibility Principle

The [Single Responsibility Principle](https://www.oodesign.com/single-responsibility-principle.html) is the first SOLID design principle, represented by the letter “S” and defined by [Robert C Martin](https://en.wikipedia.org/wiki/Robert_C._Martin). It states that in a well-designed application, each class (microservice, code module) should have only one single responsibility. Responsibility is used in the sense of having only one reason to change.

When a class handles more than one responsibility, any changes made to the functionalities might affect others. This is bad enough if you have a smaller app but can become a nightmare when you work with [complex, enterprise-level software](https://raygun.com/blog/errors-and-exceptions/). By making sure that each module encapsulates only one responsibility, you can save a lot of testing time and create a more maintainable architecture.

#### Example of the Single Responsibility Principle

Let’s see an example. I’ll use Java but you can apply SOLID design principles to any other OOP languages, too.

Say, we are writing a Java application for  a book store. We create a `Book` class that lets users get and set the titles and authors of each book, and search the book in the inventory.

```java
class Book {

	String title;
	String author;

	String getTitle() {
		return title;
	}
	void setTitle(String title) {
		this.title = title;
	}
	String getAuthor() {
		return author;
	}
	void setAuthor(String author) {
		this.author = author;
	}
	void searchBook() {...}

}
```

However, the above code violates the Single Responsibility Principle, as the `Book` class has two responsibilities. First, it sets the data related to the books (`title` and `author`). Second, it searches for the book in the inventory. The setter methods change the `Book` object, which might cause problems when we want to search the same book in the inventory.  

To apply the Single Responsibility Principle, we need to decouple the two responsibilities. In the refactored code, the `Book` class will only be responsible for getting and setting the data of the `Book` object.

```java
class Book {

	String title;
	String author;

	String getTitle() {
		return title;
	}
	void setTitle(String title) {
		this.title = title;
	}
	String getAuthor() {
		return author;
	}
	void setAuthor(String author) {
		this.author = author;
	}

}
```

Then, we create another class called `InventoryView` that will be responsible for checking the inventory. We move the `searchBook()` method here and reference the `Book` class in the constructor.

```java
class InventoryView {

  Book book;

	InventoryView(Book book) {
		this.book = book;
	}

	void searchBook() {...}

}
```

On the UML diagram below, you can see how the architecture changed after we refactored the code following the Single Responsibility Principle. We split the initial `Book` class that had two responsibilities into two classes, each having its own single responsibility.

![Single Responsibility Principle](https://raygun.com/blog/images/solid-design-principles/01-singleresponsibility-example.jpg)

## Open/Closed Principle

The [Open/Closed Principle](https://www.oodesign.com/open-close-principle.html) is the “O” of SOLID’s five software design principles. It was [Bertrand Meyer](https://en.wikipedia.org/wiki/Bertrand_Meyer) who coined the term in his book “Object-Oriented Software Construction”. The Open/Closed Principle states that classes, modules, microservices, and other code units should be open for extension but closed for modification.

So, you should be able to extend your existing code using OOP features like inheritance via subclasses and interfaces. However, you should never modify classes, interfaces, and other code units that already exist (especially if you use them in production), as it can lead to unexpected behavior. If you add a new feature by extending your code rather than modifying it, you minimize the risk of failure as much as possible. Besides, you also don’t have to unit test existing functionalities.

### Example of the Open/Closed Principle

Let’s stay with our book store example. Now, the store wants to hand out cookbooks at a discount price before Christmas. We already follow the Single Responsibility Principle, so we create two separate classes: `CookbookDiscount` to hold the details of the discount and `DiscountManager` to apply the discount to the price.

```java
class CookbookDiscount {

	String getCookbookDiscount() {

		String discount = "30% between Dec 1 and 24.";

		return discount;
	}

}

class DiscountManager {

	void processCookbookDiscount(CookbookDiscount discount) {...}

}
```

This code works fine until the store management informs us that their cookbook discount sales were so successful that they want to extend it. Now, they want to hand out every biography with a 50% discount on the subject’s birthday. To add the new feature, we create a new `BiographyDiscount` class:

```java
class BiographyDiscount {

	String getBiographyDiscount() {

		String discount = "50% on the subject's birthday.";		

		return discount;

	}
}
```

To process the new type of discount, we need to add the new functionality to the `DiscountManager` class, too:
```java
class DiscountManager {

	void processCookbookDiscount(CookbookDiscount discount) {...}

	void processBiographyDiscount(BiographyDiscount discount) {...}

}
```

However, as we changed existing functionality, we violated the Open/Closed Principle. Although the above code works properly, it might add new vulnerabilities to the application. We don’t know how the new addition would interact with other parts of the code that depends on the `DiscountManager` class. In a real-world application, this would mean that we need to test and deploy our entire app again.

But, we can also choose to refactor our code by adding an extra layer of abstraction that represents all types of discounts. So, let’s create a new interface called `BookDiscount` that the `CookbookDiscount` and `BiographyDiscount` classes will implement.

```java
public interface BookDiscount {

	String getBookDiscount();

}

class CookbookDiscount implements BookDiscount {

	@Override
	public String getBookDiscount() {
		String discount = "30% between Dec 1 and 24.";

		return discount;
	}

}

class BiographyDiscount implements BookDiscount {

	@Override
	public String getBookDiscount() {
		String discount = "50% on the subject's birthday.";

		return discount;
	}

}
```

Now, `DiscountManager` can refer to the `BookDiscount` interface instead of the concrete classes. When the `processBookDiscount()` method is called, we can pass both `CookbookDiscount` and `BiographyDiscount` as an argument, as both are the implementation of the `BookDiscount` interface.

```java
class DiscountManager {

	void processBookDiscount(BookDiscount discount) {...}
}

```

The refactored code follows the Open/Closed principle, as we could add the new `CookbookDiscount` class without modifying the existing code base. This also means that in the future, we can extend our app with other discount types (for instance, with `CrimebookDiscount`).

The UML graph below shows how our example code looks like before and after the refactoring. On the left, you can see that `DiscountManager` depends on the `CookbookDiscount` and `BiographyDiscount` classes. On the right, all three classes depend on the `BookDiscount` abstract layer (`DiscountManager` references it, while `CookbookDiscount` and `BiographyDiscount` implement it).


![Open/Closed Principle](https://raygun.com/blog/images/solid-design-principles/02-openclosed-example.jpg)


## Liskov Substitution Principle

The [Liskov Substitution Principle](https://www.oodesign.com/liskov-s-substitution-principle.html) is the third principle of SOLID, represented by the letter “L”. It was [Barbara Liskov](https://en.wikipedia.org/wiki/Barbara_Liskov) who introduced the principle in 1987 in her conference keynote talk “Data Abstraction”. The original phrasing of the Liskov Substitution Principle is a bit complicated, as it asserts that:  

> “In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).”

In layman’s terms, it states that an object of a superclass should be replaceable by objects of its subclasses without causing issues in the application. So, a child class should never change the characteristics of its parent class (such as the argument list and return types). You can implement the Liskov Substitution Principle by paying attention to the correct inheritance hierarchy.

#### Example of the Liskov Substitution Principle

Now, the book store asks us to add a new delivery functionality to the application. So, we create a `BookDelivery` class that informs customers about the number of locations where they can collect their order:


```java
class BookDelivery {
	String titles;
	int userID;

	void getDeliveryLocations() {...}
}
```

However, the store also sells fancy hardcovers they only want to deliver to their high street shops. So, we create a new `HardcoverDelivery` subclass that extends `BookDelivery` and overrides the `getDeliveryLocations()` method with its own functionality:
```java
class HardcoverDelivery extends BookDelivery {

	@Override
	void getDeliveryLocations() {...}

}
```

Later, the store asks us to create delivery functionalities for audiobooks, too. Now, we extend the existing `BookDelivery` class with an `AudiobookDelivery` subclass. But, when we want to override the `getDeliveryLocations()` method, we realize that audiobooks can’t be delivered to physical locations.

```java
class AudiobookDelivery extends BookDelivery {

	@Override
	void getDeliveryLocations() {/* can't be implemented */}
}
```

We could change some characteristics of the `getDeliveryLocations()` method, however, that would violate the Liskov Substitution Principle. After the modification, we couldn’t replace the `BookDelivery` superclass with the `AudiobookDelivery` subclass without breaking the application.

To solve the problem, we need to fix the inheritance hierarchy. Let’s introduce an extra layer that better differentiates book delivery types. The new `OfflineDelivery` and `OnlineDelivery` classes split up the `BookDelivery` superclass. We also move the `getDeliveryLocations()` method to `OfflineDelivery` and create a new `getSoftwareOptions()` method for the `OnlineDelivery` class (as this is more suitable for online deliveries).

```java
class BookDelivery {

	String title;
	int userID;

}

class OfflineDelivery extends BookDelivery {

	void getDeliveryLocations() {...}

}

class OnlineDelivery extends BookDelivery {

	void getSoftwareOptions() {...}

}
```

In the refactored code, `HardcoverDelivery` will be the child class of `OfflineDelivery` and it will override the `getDeliveryLocations()` method with its own functionality.

`AudiobookDelivery` will be the child class of `OnlineDelivery` which is good news, as now it doesn’t have to deal with the `getDeliveryLocations()` method. Instead, it can override the `getSoftwareOptions()` method of its parent with its own implementation (for instance, by listing and embedding available audio players).

```java
class HardcoverDelivery extends OfflineDelivery {

	@Override
	void getDeliveryLocations() {...}

}

class AudiobookDelivery extends OnlineDelivery {

	@Override
	void getSoftwareOptions() {...}

}
```

After the refactoring, we could use any subclass in place of its superclass without breaking the application.

On the UML graph below, you can see that by applying the Liskov Substitution Principle, we added an extra layer to the inheritance hierarchy. While the new architecture is more complex, it provides us with a more flexible design.

![Liskov Substitution Principle](https://raygun.com/blog/images/solid-design-principles/03-liskov-example.jpg)

## Interface Segregation Principle

The [Interface Segregation Principle](https://www.oodesign.com/interface-segregation-principle.html) is the fourth SOLID design principle represented by the letter “I” in the acronym. It was Robert C Martin who first defined the principle by stating that “clients should not be forced to depend on methods they don’t use.” By clients, he means classes that implement interfaces. In other words, interfaces shouldn’t include too many functionalities.

The violation of Interface Segregation Principle harms code readability and forces programmers to write dummy methods that do nothing. In a well-designed application, you should avoid interface pollution (also called fat interfaces). The solution is to create smaller interfaces that you can implement more flexibly.

### Example of the Interface Segregation Principle

Let’s add some user actions to our online bookstore so that customers can interact with the content before making a purchase. To do so, we create an interface called `BookAction` with three methods: `seeReviews()`, `searchSecondHand()`, and `listenSample()`.

```java
public interface BookAction {

	void seeReviews();
	void searchSecondhand();
	void listenSample();

}
```

Then, we create two classes: `HardcoverUI` and an `AudiobookUI` that implement the `BookAction` interface with their own functionalities:

```java
class HardcoverUI implements BookAction {

	@Override
	public void seeReviews() {...}

	@Override
	public void searchSecondhand() {...}

	@Override
	public void listenSample() {...}

}

class AudiobookUI implements BookAction {

	@Override
	public void seeReviews() {...}

	@Override
	public void searchSecondhand() {...}

	@Override
	public void listenSample() {...}

}
```

Both classes depend on methods they don’t use, so we have broken the Interface Segregation Principle. Hardcover books can’t be listened to, so the `HardcoverUI` class doesn’t need the `listenSample()` method. Similarly, audiobooks don’t have second-hand copies, so the `AudiobookUI` class doesn’t need it, either.

However, as the `BookAction` interface include these methods, all of its dependent classes have to implement them. In other words, `BookAction` is a polluted interface that we need to segregate. Let’s extend it with two more specific sub-interfaces: `HardcoverAction` and `AudioAction`.

```java
public interface BookAction {

	void seeReviews();

}

public interface HardcoverAction extends BookAction {

	void searchSecondhand();

}

public interface AudioAction extends BookAction {

	void listenSample();

}
```

Now, the `HardcoverUI` class can implement the `HardcoverAction` interface and the `AudiobookUI` class can implement the `AudioAction` interface.

This way, both classes can implement the `seeReviews()` method of the `BookAction` super-interface. However, `HardcoverUI` doesn’t have to implement the irrelevant `listenSample()` method and `AudioUI` doesn’t have to implement `searchSecondhand()`, either.

```java
class HardcoverUI implements HardcoverAction {

	@Override
	public void seeReviews() {...}

	@Override
	public void searchSecondhand() {...}

}

class AudiobookUI implements AudioAction {

	@Override
	public void seeReviews() {...}

	@Override
	public void listenSample() {...}

}
```

The refactored code follows the Interface Segregation Principle, as neither classes depend on methods they don’t use. The UML diagram below excellently shows that the segregated interfaces lead to simpler classes that only implement the methods they really need:

![Interface Segregation Principle](https://raygun.com/blog/images/solid-design-principles/04-interface-segregation-example.jpg)

## Dependency Inversion Principle

The [Dependency Inversion Principle](https://www.oodesign.com/dependency-inversion-principle.html) is the fifth SOLID design principle represented by the last “D” and introduced by Robert C Martin. The goal of the Dependency Inversion Principle is to avoid tightly coupled code, as it easily breaks the application. The principle states that:

 >  “High-level modules should not depend on low-level modules. Both should depend on abstractions.”

 >  “Abstractions should not depend on details. Details should depend on abstractions.”

In other words, you need to decouple high-level and low-level classes. High-level classes usually encapsulate complex logic while low-level classes include data or utilities. Typically, most people would want to make high-level classes depend on low-level classes. However, according to the Dependency Inversion Principle, you need to invert the dependency. Otherwise, when the low-level class is replaced, the high-level class will be affected, too.

As a solution, you need to create an abstract layer for low-level classes, so that high-level classes can depend on abstraction rather than concrete implementations.

Robert C Martin also mentions that the Dependency Inversion Principle is a specific combination of the Open/Closed and Liskov Substitution Principles.

### Example of the Dependency Inversion Principle

Now, the book store asked us to build a new feature that enables customers to put their favorite books on a shelf.

To implement the new functionality, we create a lower-level `Book` class and a higher-level `Shelf` class. The `Book` class will allow users to see reviews and read a sample of each book they store on their shelves. The `Shelf` class will let them add a book to their shelf and customize the shelf.

```java
class Book {

	void seeReviews() {...}
	void readSample() {...}

}

class Shelf {

	Book book;

	void addBook(Book book) {...}
	void customizeShelf() {...}

}
```

Everything looks fine, but as the high-level `Shelf` class depends on the low-level `Book`, the above code violates the Dependency Inversion Principle. This becomes clear when the store asks us to enable customers to add DVDs to their shelves, too. To fulfill the demand, we create a new `DVD` class:
```java
class DVD {

	void seeReviews() {...}
	void watchSample() {...}

}
```

Now, we should modify the `Shelf` class so that it can accept DVDs, too. However, this would clearly break the Open/Closed Principle.
The solution is to create an abstraction layer for the lower-level classes (`Book` and `DVD`). We’ll do so by introducing the `Product` interface that both classes will implement.
```java
public interface Product {

	void seeReviews();
	void getSample();

}

class Book implements Product {

	@Override
	public void seeReviews() {...}

	@Override
	public void getSample() {...}

}

class DVD implements Product {

	@Override
	public void seeReviews() {...}

	@Override
	public void getSample() {...}

}
```

Now, `Shelf` can reference the `Product` interface instead of its implementations (`Book` and `DVD`). The refactored code also allows us to later introduce new product types (for instance, `Magazine`) that customers can put on their shelves, too.
```java
class Shelf {

	Product product;

	void addProduct(Product product) {...}

	void customizeShelf() {...}

}
```

The above code also follows the Liskov Substitution Principle, as the `Product` type can be substituted with both of its subtypes (`Book` and `DVD`) without breaking the program. At the same time, we have also implemented the Dependency Inversion Principle, as in the refactored code, high-level classes don’t depend on low-level classes, either.

As you can see on the left of the UML graph below, the high-level `Shelf` class depends on the low-level `Book` before the refactoring. Without applying the Dependency Inversion Principle, we should make it depend on the low-level `DVD` class, too. However, after the refactoring, both the high-level and low-level classes depend on the abstract `Product` interface (`Shelf` refers to it, while `Book` and `DVD` implement it).

![Dependency Inversion Principle](https://raygun.com/blog/images/solid-design-principles/dependency-inversion.jpg)

## How should you implement SOLID design principles?

Implementing the SOLID design principles increases the overall complexity of a code base, but it leads to more flexible design. Besides monolithic apps, you can also apply SOLID design principles to [microservices](https://raygun.com/blog/what-are-microservices/) where you can treat each microservice as a standalone code module (like a class in the above examples).

When you break a SOLID design principle, Java and other compiled languages might throw an [Exception](https://raygun.com/blog/java-exceptions-terminology/), but it doesn’t always happen. Software architecture problems are hard to detect, but advanced diagnostic software such as [APM tools](https://raygun.com/platform/apm) can provide you with many useful hints.