Understanding Clean architecture with Example in Java

Shivank Goyal
8 min readNov 5, 2020

--

What Is Clean Architecture?

Clean architecture is a software design philosophy that separates the elements of a design into ring levels. The main rule of clean architecture is that code dependencies can only come from the outer levels inward. Code on the inner layers can have no knowledge of functions on the outer layers. The variables, functions and classes (any entities) that exist in the outer layers can not be mentioned in the more inward levels. It is recommended that data formats also stay separate between levels.

Clean Architecture divides our system into four layers, usually represented by circles:

  • Entities, which contain enterprise-wide business rules. You can think of them as about Domain Entities.
  • Use cases, which contain application-specific business rules. These would be counterparts to Application Services with the caveat that each class should focus on one particular Use Case.
  • Interface adapters, which contain adapters to peripheral technologies. Here, you can expect MVC, Gateway implementations and the like.
  • Frameworks and drivers, which contain tools like databases or framework. By default, you don’t code too much in this layer, but it’s important to clearly state the place and priority that those tools have in your architecture.

The Essence of Clean Architecture

I see two things that make Clean Architecture distinct and potentially more effective than other architectural styles: strong adherence to the Dependency Inversion Principle and Use Case orientation.

Strong Adherence to DIP

Clean Architecture introduces the Dependency Inversion Principle at the architectural level. This way, it explicitly states the priorities between different kinds of objects in your system. In a way, Clean Architecture does a better job at this, as it leaves no doubt about the tools like frameworks or databases — they have a dedicated layer outside all others.

Use Case Orientation

Clean Architecture promotes vertical slicing of the code and leaving the layers mostly at the class level. It reorients the packaging towards Use Cases. This is important as, ultimately, in any application that has some sort of GUI, one could identify real Use Cases. It’s also important to note that entities sit in a different layer as in complex systems, one Use Case can orchestrate several entities to cooperate and categorizing it by the type of the entity would be artificial.

The idea of Clean Architecture is to put delivery and gateway at the edges of our design. Business logic should not depend on whether we expose a REST or a GraphQL API, and it should not depend on where we get data from — a database, a microservice API exposed via gRPC or REST, or just a simple CSV file.

The pattern allows us to isolate the core logic of our application from outside concerns. Having our core logic isolated means we can easily change data source details without a significant impact or major code rewrites to the codebase.

One of the main advantages we also saw in having an app with clear boundaries is our testing strategy — the majority of our tests can verify our business logic without relying on protocols that can easily change.

Components

I wanted to give a more pragmatic/simplistic approach that can help in the first incursion to the clean architecture. That’s why I’ll be omitting concepts that may feel unavoidable to architecture purists.

My only goal here is that you understand what I consider the main (and most complicated) topic in clean architecture:

Component Model

Core

The “core” will be described in the Model Entities and Use Case component. Here is the domain knowledge and the “feature” we want to implement.

Model Entities

Entities encapsulate Enterprise wide business rules.

An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter as long as the entities can be used by many different applications in the enterprise. They represents the bare domain.

Use cases

which contain application-specific business rules. These would be counterparts to Application Services with the caveat that each class should focus on one particular Use Case.

It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.

Infrastructure

All this is just because computer programming is not perfect, we need a lot of support to let the core work in a real environment. These components are known as the “Infrastructure” is composed by :

Delivery

In the Delivery we found the Interface Adapters that receives requests from the outside of the microservice and delivers a response to them.

It is common to implement it as a REST HTTP Server, or consume Message from some Message Broker, or Job Schudeler

Gateways

In the Delivery we found the Interface Adapters that receives requests from the outside of the microservice and delivers (that’s where the name comes from) a response to them.

It is common to implement them as a REST HTTP Clients, Message Broker Producer, or any other API Client.

Repositories

These are Interface Adapters to systems meant to store and retrieve (serialized) application objects (usually entity).

Repository are the interfaces to getting entities as well as creating and changing them. They keep a list of methods that are used to communicate with data sources and return a single entity or a list of entities. (e.g. UserRepository)

Configuration

The Configuration is the part of the system that composes the different components into a running system.

In this module we create the application according to the implementations that we are developing and the behavior that we want to give to it.

Main

The main is just the entry point to the application.

Process flow

To provide the desired behaviour, these components interact between them. The flow of actions, usually follows this pattern:

Request Process Flow

  1. An external system performs a request (An HTTP request, a JMS message is available, etc.)
  2. The Delivery creates various Model Entities from the request data
  3. The Delivery calls an Use Case execute
  4. The Use Case operates on Model Entities
  5. The Use Case makes a request to read / write on the Repository
  6. The Repository consumes some Model Entity from the Use Case request
  7. The Repository interacts with the External Persistence (DB,Cache,etc)
  8. The Repository creates Model Entities from the persisted data
  9. The Use Case requests collaboration from a Gateway
  10. The Gateway consumes the Model Entities provided by the Use Case request
  11. The Gateway interacts with External Services (other API, microservices, put messages in queues, etc.)

Java Implementation

Model entities

The domain objects (e.g., a Movie or a Shooting Location) — they have no knowledge of where they’re stored.

Model Entity

Use Case

are classes that orchestrate and perform domain actions — think of Service Objects or Use Case Objects. They implement complex business rules and validation logic specific to a domain action (e.g., onboarding a production)

Example the use case:

By having business logic extracted into use case, we are not coupled to a particular transport layer or controller implementation. The use case can be triggered not only by a controller, but also by an event, a cron job, or from the command line.

Delivery

Transport Layer can trigger an interactor to perform business logic. We treat it as an input for our system. The most common transport layer for microservices is the HTTP API Layer and a set of controllers that handle requests.

Each Handler handles a specific requests, converts the request payload to DTO or model entities, and calls the appropriate execute. Finally, it converts the execute response into the response payload and sends it back it to the caller.

Gateways

Here the Gateway takes advantage of a provided RestClient to connect to an external service.

The Gateway has no Business logic, but conversions and mappings.

Repositories

A data source implements methods defined on the repository and stores the implementation of fetching and pushing the data.

Configuration

This example of code, there the configuration of the behavior of all our layers. So we send a bean to all those beans that depend on it.

Main

Finally, the main class ends being a very boring one:

Package structure

This pragmatic architecture is separated in two sub-projects, core and infrastructure:

Core

Domain and Application layers both represent core, however, their nature is of 2 kinds:

Domain business logic: here you find the “models” of your app, which can be of different types (Aggregate Roots, Entities, Value Objects) and that implement enterprise-wide business rules (they not only contain data but also processes).

Application business logic: here you find the so-called “usecases”, situated on top of models and the “ports” for the Data Layer (used for dependency inversion, usually Repository interfaces), they retrieve and store domain models by using either repositories or other Use Cases.

Infrastructure

Is necessary lot of support to let the core work in a real environment. These components are known as the “Infrastructure”.

Persistence might be an adapter to a SQL database (an Active Record class in Rails or JPA in Java), an elastic search adapter, REST API, or even an adapter to something simple such as a CSV file or a Hash. A data source implements methods defined on the repository and stores the implementation of fetching and pushing the data.

Delivery can trigger an interactor to perform business logic. We treat it as an input for our system. The most common transport layer for microservices is the HTTP and a set of controllers that handle requests. By having business logic extracted into use case, we are not coupled to a particular transport layer or controller implementation. Use case can be triggered not only by a controller, but also by an event, a cron job, or from the command line.

Conclusion

We are in a great position when it comes to swapping data sources to different microservices. One of the key benefits is that we can delay some of the decisions about whether and how we want to store data internal to our application. Based on the feature’s use case, we even have the flexibility to determine the type of data store — whether it be Relational or Documents.
At the beginning of a project, we have the least amount of information about the system we are building. We should not lock ourselves into an architecture with uninformed decisions leading to a project paradox.
The decisions we made make sense for our needs now and have enabled us to move fast. The best part of clean architecture is that it keeps our application flexible for future requirements to come.

--

--

No responses yet