Clean Architecture

Clean Architecture

Now, I am beginning series of related articles needed to create mid to big applications in a scalable, maintainable and testable way.

Table of contents:

What is Clean Architecture?

There are many articles out there that explain Clean Architecture, but still, everyday new articles are out on the subject. When speech goes about Clean Architecture, we all remember the diagram below.

Some engineers think whole Clean Architecture's idea is all this and try to learn the picture by heart and retell the same picture when asked to explain, without understanding it. However, there is deep idea behind Clean Architecture and this photo is a single implementation of it.

Clean Architecture is a software design philosophy that emphasizes organizing the system in a way that separates concerns, promotes scalability, and makes systems easier to test and maintain. It was introduced by Robert C. Martin (Uncle Bob).

Let's talk about the highlighted principles above with understandable terms. We have to build a car. We don't build all the details of a car in one place rather a car consists of many components built separately. For example, car has wheels, engine, body, seats, radio, software which are all created in separate factories - separating concerns.

Now, the question is why we need to separate concerns? Let's say we want to produce more cars. We see that we have all the details but lacking engines to build more cars. What we do is to enlarge the engine factory to make it capable of producing more engines. We achieve this by breaking the car into components. We see that our factories are scalable individually now.

When we separate details, we can always improve the quality by testing them in place. Let's say in a factory that creates radio for our car, the focus of the factory is on only one thing - radio. They try to perfect their product, they know what and how to test. Moreover, if let's say there's defect in the product they found out after production, the factory can easily provide maintenance for the particular component.

So, obeying Clean Architecture principles, in our mobile application world, we mainly divide the application into presentation, domain and data layers.

Presentation Layer

The role of this layer is to display application data on the screen. This layer is responsible to react to user interactions, pass the interaction to domain layer or reflect the changes coming from data layer. It consists of two components: UI elements (widgets/dialogs/bottom-sheets) and UI state holders (Bloc/ViewModel).

UI elements are responsible for displaying the data on screen through widgets, dialogs, bottom-sheets and more, it is an interface to get user interactions as well and pass the interactions to domain layer. They should not contain business logic, as this resides in the domain layer. UI elements can be tested through golden tests, just FYI.

UI state holders stay behind UI elements and keep the state, handle UI logic, mediate between the view and the underlying application logic. Some examples are ViewModels, Presenters, Controllers or Bloc for Flutter. By separating UI elements and UI state holders, the system gains separate testability as well.

Domain Layer

This layer is core of the system, it does not depend anything, but Presentation and Data Layers depend on it. This layer holds the business logic of the application. It consists of but not limited to Use Cases, Manager Classes, Repositories and Entities. Each class does a separate job, holds core business value.

Use Case - being single responsible, defines the steps and rules required to perform a task or achieve a business goal in response to user interactions or external triggers. It orchestrates interactions between entities and external systems (e.g., databases, APIs, or user interfaces). It also handles success and failed cases. Since use cases don’t rely on external systems, they can be tested independently and can be reused.

Manager Classes are responsible for handling business logic and coordinating complex operations within the domain layer of an application. These classes encapsulate workflows, policies, or rules that involve one or more domain entities.

Repository Interfaces act as an intermediary between the domain layer and the data layer, abstracting away the details of data storage and providing a clean interface for the domain logic.

For example, let's say we have Book Repository Interface and it holds several methods to do operations with book.

abstract interface class IBookRepository {
    Future<BookEntity> getBookById(String bookId);

    Future<BookEntity> downloadBook(String bookId);

    Future<List<BookEntity>> getAuthorBooks(String authorId);
}

So, from the IBookRepository interface, what is important for us is that we can fetch or download book by id, how is not important - which is the job of data layer to take care of.

Entities are objects that encapsulate core business logic and are identified by a unique identifier rather than their attributes or behavior. They represent the foundational business concepts of the system. Entities encapsulate both state (attributes) and behavior (business logic). They are not just data containers, they enforce rules and constraints. Entities typically have a lifecycle and persist over time (e.g., a Customer may register, place orders, and update their details). Let's see what BookEntity would look like:

class BookEntity {

    final String id;
    final String isbnNumber;
    final String author;
    final String title;
    final String subtitle;
    ...
}

Data Layer

This layer is source of truth for the data that we use within our application. It is used to save our data in local storage, get data from network, get access to native android/iOS sides and many more. This layer passes necessary data to domain layer through domain layer repository implementations. It consists of but not limited to Local Storage, Remote Source files and many more.

Local Storage refers to mechanisms that we use to persist data locally on a device. This is often necessary for offline functionality, caching, or temporary storage. Local Storage is an abstraction that enables interaction with underlying storage mechanisms like files, databases, or platform-specific storage (e.g., Android's SharedPreferences or iOS's UserDefaults). There are a number of ways we can use to store our data on a device.

  1. File Storage - for storing binary files, documents, or other unstructured data. Examples: Images, PDFs.
  2. Key-Value Storage - lightweight storage for simple data like user preferences. Examples: SharedPreferences (Android), UserDefaults (iOS), localStorage (Web).
  3. SQLite/Relational Databases - for structured data requiring queries or relationships. Examples: sqflite, Room (Android).
  4. NoSQL Databases - for unstructured or semi-structured data. Examples: Hive, Isar.
  5. Object Storage - for storing serialized domain objects. Examples: JSON files, proprietary formats.

Remote Source refers to mechanisms that we use communicate with backend and cloud storages over a network, that is abstract to the application. This could include APIs, microservices, or other remote servers that provide data or perform actions. We just believe through calling the particular endpoints using particular protocols we get our desired result. In Clean Architecture, the data layer bridges the gap between the domain and external systems through Remote Source. Through Remote Source our application can fetch and send data, authenticate the user, receive real-time updates or run server procedures.

When data received from backend over a network is to be transferred to domain, it is data layer's duty to convert the received data to format necessary to domain layer, which means this layer is also dependent on domain layer.

Repository Implementations are classes that implement repository interfaces defined in domain layer, as we talked above. In domain layer, repository interfaces were meant to outline what is important, but now, we bring onto the table how they do the job.

Summary

Clean Architecture is a big topic to talk about. Clean Architecture itself is a set of principles, we may say, to use to make the system scalable, testable and maintainable through separation of concerns. How is so? Well, we talked about presentation, domain and data layers. Following that, we continued separating each layer into further components to achieve single responsibility from SOLID. When components serve single responsibility, they are easily replaceable, maintainable and testable. We can think of each component as separate world and there are many tools to test each component.

What is next?

In upcoming articles, I'm going to outline each layer that we talked about in this article, in case of real application where we will see best practices and write code.

References: