A Deep Dive into Clean Architecture and Solid Principles

Building Software for the Future

Amandeep Singh
9 min readOct 30, 2023
Photo by No Revisions on Unsplash

In the world of software development, creating robust and maintainable systems is a paramount goal. Achieving this requires a profound understanding of clean architecture and SOLID principles. In this article, we will delve into the “WWWWH” of clean architecture and SOLID principles, exploring What they are, When they emerged, Why they are crucial, Where they can be applied, and How they can be implemented.

Let's learn about WWWWH

What?

  1. The software Architecture Model aims to the separation of concerns of the System.
  2. The Main Idea is to separate the Heart of the Software i.e. Business Rules Implementation to the Rest of the System like Databases, networks, UI, and any Other external Systems.
  3. Clean architecture is a category of software design pattern for software architecture that follows the concepts of clean code and implements SOLID principles.

When?

  1. The concept of clean code and clean architecture was originally coined in 2008 by Robert C. Martin, known in the community as “Uncle Bob”.
  2. Uncle Bob has shared his idea of clean architecture with 3 basic layers namely Data, Domain, and Presentation to achieve this idea of Separation of Concerns to a great extent.

Why?

Clean architecture is crucial for several reasons. It greatly enhances code maintainability by organizing code logically and reducing dependencies. It promotes testability, making it easier to write unit tests and validate the correctness of the system. Clean architecture also ensures adaptability and scalability, as changes to one component don’t ripple through the entire system.

Where?

Clean architecture is highly versatile and can be applied in a wide range of software development contexts. It’s not limited to any specific technology or platform, making it suitable for mobile app development, web applications, desktop software, and any other domain where software engineering is involved.

How?

Implementing clean architecture involves adhering to several key characteristics. It should be independent of frameworks and technologies, ensuring that components can be replaced without affecting the core functionality. The architecture should be highly testable, with minimal dependencies on the user interface, database, and external systems, enabling easy unit testing. This independence from external agencies allows for flexibility and adaptability when external components change.

Not only clean but any software architecture must have the following characteristics :

  • Independent of frameworks
  • Testable
  • Independent of the UI
  • Independent of the database
  • Independent of any external agency

The Problem with Traditional Architectures

It’s crucial to understand the issues with traditional software architectures. In many cases, traditional architectures tightly couple the application’s core logic with external dependencies such as databases, frameworks, and user interfaces. This tight coupling makes it challenging to make changes, add new features, or even switch out components without affecting the entire system.

Additionally, testing can be a nightmare in such architectures. The lack of separation between core business logic and external components makes unit testing difficult and often leads to brittle, error-prone code. These challenges hinder the maintainability and adaptability of software applications.

The Principles of Clean Architecture

Clean Architecture addresses these issues by emphasizing the following key principles:

  1. Separation of Concerns: Clean Architecture enforces a clear separation between the application’s core business logic and external details. This separation is achieved through layers, each with distinct responsibilities. The innermost layer contains the essential business rules, while the outer layers deal with technical implementation and delivery mechanisms.
  2. Dependency Rule: In Clean Architecture, dependencies flow inward toward the core business logic. This means that high-level modules are not dependent on low-level modules but, rather, the other way around. This principle ensures that changes in the external components do not affect the core business logic.
  3. Testability: Clean Architecture encourages writing unit tests for the business logic independently of external dependencies. This separation allows for efficient and comprehensive testing of the core functionality without requiring integration tests for every minor change.
  4. Platform Independence: Clean Architecture ensures that the business logic is independent of the framework or platform used for implementation. This makes it easier to switch out technologies or adapt the application to various platforms without rewriting the core logic.

Components of Clean Architecture

Business Rules (Heart of Any Software)

Business rules are the essential procedures and policies that have a direct impact on a business’s profitability or operational efficiency. These rules remain consistent, whether applied within a computer system or executed manually. For instance, a bank’s calculation of interest rates is a critical business rule as it operates on critical business data.

Entities

Entity

Entities are a central concept in clean architecture, representing groups of business rules that operate on critical business data. They often have direct and efficient access to this critical data, making them a pivotal part of the core business logic. Entities are responsible for encapsulating and safeguarding the integrity of vital business information.

Use Cases

Use case

Use cases are a specific type of business rule that may not be pure, unlike entities. They are designed to be executed within an automated system and are not suitable for manual execution. For instance, the process of gathering user information before creating a new loan is a use case.

Interface Adapters

These adapters convert data between the use cases and external frameworks and tools. They include presenters, controllers, and presenters. This layer is where you integrate with the framework and deal with user interfaces and databases.

Frameworks and Drivers:

The outermost layer contains the delivery mechanisms and external frameworks, including web servers, databases, and UI components. These components interact with the interface adapters to handle data input and output.

Clean Architecture Layes
Data Flow

Benefits of Clean Architecture

  1. Testability: Clean Architecture promotes testability by isolating the core business logic in the use case layer. You can write unit tests for use cases without needing to deal with database or UI components.
  2. Maintainability: Clean Architecture makes it easier to modify and extend the application without affecting other parts. Changes in the external adapters do not impact the inner core.
  3. Decoupling: It encourages loose coupling between different components, making it easier to replace or upgrade external systems, such as databases or frameworks.
  4. Scalability: With a well-structured application, it’s simpler to scale specific parts of your system, like adding new features or microservices.

SOLID Principles

The SOLID principles are a valuable set of guidelines for object-oriented design, helping developers create maintainable, extensible, and modular code. By following these principles, you can enhance code quality, reduce bugs, and make it easier to adapt to changing requirements. Whether you’re working on a small project or a large-scale application, understanding and applying these principles is a crucial step towards becoming a more effective and skilled software developer.

S RP : The Single Responsibility Principle
O CP : The Open-Closed Principle
L SP : The Liskov Substitution Principle
I SP : The Interface Segregation Principle
D IP : The Dependency Inversion Principle

1. Single Responsibility Principle (SRP):

The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should have a single responsibility, and it should encapsulate that responsibility. Let’s consider an example in the context of a text editor:

Bad Example:

class TextEditor {
public void spellCheck() {
// Code for spell checking
}

public void formatText() {
// Code for text formatting
}
}

In this case, the TextEditor class violates the SRP because it has two distinct responsibilities: spell checking and text formatting.

Good Example:

class TextEditor {
public void formatText() {
// Code for text formatting
}
}

class SpellChecker {
public void spellCheck() {
// Code for spell checking
}
}

By separating the responsibilities into different classes, we adhere to the SRP, making the code more maintainable and flexible.

2. Open-Closed Principle (OCP):

The Open-Closed Principle suggests that software entities should be open for extension but closed for modification. In other words, you should be able to extend the behavior of a class without changing its source code. Here’s an example with geometric shapes:

Bad Example:

class Rectangle {
public double calculateArea() {
// Code for calculating the area of a rectangle
}
}

class Circle {
public double calculateArea() {
// Code for calculating the area of a circle
}
}

In this example, if we want to add a new shape, like a triangle, we would need to modify the existing classes, violating the OCP.

Good Example:

interface Shape {
double calculateArea();
}

class Rectangle implements Shape {
public double calculateArea() {
// Code for calculating the area of a rectangle
}
}

class Circle implements Shape {
public double calculateArea() {
// Code for calculating the area of a circle
}
}

With the introduction of the Shape interface, we can easily create new shape classes without modifying the existing ones.

3. Liskov Substitution Principle (LSP):

The Liskov Substitution Principle states that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program. Consider a classic example involving shapes:

Bad Example:

class Rectangle {
protected int width;
protected int height;

public void setWidth(int width) {
this.width = width;
}

public void setHeight(int height) {
this.height = height;
}
}

class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}

@Override
public void setHeight(int height) {
this.height = height;
this.width = height;
}
}

In this case, substituting a Square object for a Rectangle object can lead to unexpected behavior. This violates the LSP.

Good Example:

class Shape {
protected int width;
protected int height;

public void setWidth(int width) {
this.width = width;
}

public void setHeight(int height) {
this.height = height;
}
}

class Rectangle extends Shape {
// No need to override setWidth and setHeight
}

class Square extends Shape {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}

@Override
public void setHeight(int height) {
this.height = height;
this.width = height;
}
}

In this improved example, we follow the Liskov Substitution Principle, ensuring that a Square is a valid substitute for a Rectangle.

4. Interface Segregation Principle (ISP):

The Interface Segregation Principle emphasizes that clients should not be forced to depend on interfaces they do not use. Let’s look at an example with a multifunctional printer:

Bad Example:

interface Printer {
void print();
void scan();
void fax();
}

class MultiFunctionalPrinter implements Printer {
@Override
public void print() {
// Code for printing
}

@Override
public void scan() {
// Code for scanning
}

@Override
public void fax() {
// Code for faxing
}
}

In this scenario, clients that only need to print are burdened with unnecessary methods. This violates the ISP.

Good Example:

interface Printer {
void print();
}

interface Scanner {
void scan();
}

interface Fax {
void fax();
}

class MultiFunctionalPrinter implements Printer, Scanner, Fax {
@Override
public void print() {
// Code for printing
}

@Override
public void scan() {
// Code for scanning
}

@Override
public void fax() {
// Code for faxing
}
}

By breaking down the monolithic Printer interface into smaller, focused interfaces, we adhere to the ISP, ensuring that clients only depend on the methods they need.

5. Dependency Inversion Principle (DIP):

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Consider a logging system:

Bad Example:

class Logger {
public void log(String message) {
// Code for logging to a file
}
}

class UserController {
private Logger logger = new Logger();

public void createUser(String username) {
try {
// Code for creating a user
} catch (Exception e) {
logger.log("Error creating user: " + e.getMessage());
}
}
}

In this example, the UserController is tightly coupled to the Logger class, making it hard to change the logging mechanism.

Good Example:

interface Logger {
void log(String message);
}

class FileLogger implements Logger {
public void log(String message) {
// Code for logging to a file
}
}

class UserController {
private Logger logger;

public UserController(Logger logger) {
this.logger = logger;
}

public void createUser(String username) {
try {
// Code for creating a user
} catch (Exception e) {
logger.log("Error creating user: " + e.getMessage());
}
}
}

In the improved version, the UserController depends on an abstraction (the Logger interface) rather than a concrete implementation, adhering to the Dependency Inversion Principle.

Important Links

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

--

--

Amandeep Singh

👓 Software Engineer | 📚 Lifelong Learner | 🧩 Problem Solver | 🔧 Process Engineer | 🏗️ App Architect | ☕ Java Junkie