A simplified version of Clean Arch
Tags: designsystem, architecture
Published: 2024-06-06 12:22:20

The Clean Arch created by Uncle Bob is amazingly useful and robust, but it's very complex for beginners and very costly to maintain, mainly for small startups.

In this article, I'll explain "my version" of Clean Arch, that tries it's best to simplify the original architecture while maintaining good part of it's robustness.

One thing to emphasize is: This is an **architecture pattern** so it's language agnostic and framework agnostic. You can use it in any language, framework, library, project, that you want to.

If you want to know more about Clean Arch, you can [read the book](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164) or [read this article](https://betterprogramming.pub/the-clean-architecture-beginners-guide-e4b7058c1165).

## Dictionary

Before we start with the architecture, it's very important to define the meaning of some words.

### Contracts and Implementations

**Contracts** are specifications for how to implement something. They don't do anything, they only tell you how to do something, but unlike your project manager, it tell how to do it in the right way. They can be view as abstract classes, interfaces or types.

On the other hand, **Implementations** are the things that really do something, following the instructions of the **Contracts**. They can be view as classes or functions.

### Domains

Domain are a very subjective concept, it's up to you to analyse your business, its requirements and define what should be a domain.

Some examples of domains for a social network:
- Auth
- User
- Post
- Comment
- Like

## The core concepts

![henriqueleite42 Clean Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/acxrxq0zy23xmpufblmx.png)

### Controllers

This is the layer to expose your application to the users, it's the only part that the user has access to. It can be a HTTP server, a GraphQL server, a Queue handler, a Cron Job, anything that can call your application.

This layer is responsible for:
- Receiving the requests (pooling for messages on queues, etc)
- Validating the inputs using the [Validators](#validators) layer and handling the validation error if it happens
- Calling the [UseCases](#usecases) with the validated input
- Returning the response (UseCase output) in the correct format (including errors)

### Validators

This is the layer to validate the inputs: Ensure that all the necessary data is being received, is in the correct format with the correct types, very simple, very slim.

### Models

Models are the [Contracts](#contracts-and-implementations) for the [Repositories]($repositories) and [Usecases](#usecases).

### UseCases

This is where all your business rules are at: All the "Can user do this", "If this happens, do this". The UseCases are the "most complex"/biggest part of our system, because it is responsible for using all the other components ([Repositories](#repositories) and [Adapters](#adapters)) to build a flow to execute something.

### Repositories

This layer is responsible for communicating with the database, wrapping your queries (with veeery little of business rules, only the simplest ones, examples below) and abstracting them on simple methods that can be used for your [UseCases](#usecases).

### Adapters

Adapters are responsible for abstracting / wrapping external libraries, APIs and dependencies in general. They can be used both from [Repositories](#repositories) and [UseCases](#usecases).

## How do we build something using this architecture?

- Create a POC: A minimalist version of what we are trying to build, without worry for code readability, mutability or struct, just put everything in one function in one file, used to learn the requirements for the final product.
- Using the knowledge got from the last step, we create all the [Contracts](#contracts-and-implementations) ([Models](#models)) that we need (including the one for the adapters)
  - We usually start from the contracts for the [Adapters](#adapters)
  - And then create the [Models](#models)
- Implement the [Adapters](#adapters) following their [Contracts](#contracts-and-implementations)
- Implement the [Repositories](#repositories) following their [Contracts](#contracts-and-implementations)
- Implement the [UseCases](#usecases) following their [Contracts](#contracts-and-implementations)
- Implement the [Validators](#validators) that you will need
- Implement the [Delivery](#delivery) layer (HTTP server with it's routes, queue system, cron, etc)
- Put everything together and it's done!

## Example

I'll provide you guys an example in Golang, but this can be applied to any language and framework.

**BUT REMEMBER:** This is an extremely simple example that **SHOULD NOT BE USED IN PRODUCTION!!!**

### Folders Structure

Let's start from the folders structure:

![Folders Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36ysczsr61px2w3ter65.png)

As you can see, it's very basic and straight forward: it has one folder for each core concept of the architecture.

### Models Example

![Models Example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mjo1v5on9ci85roeaaq2.png)

Not all models must have [UseCases](#usecases), the same way that not all models must have [Repositories](#repositories) or Entities (representations of the database tables, used to type things usually returned by the Repositories).

In our example we have 2 models: User and Auth.

```go
// internal/models/user.go

package models

// ----------------------------
//
//      Repository
//
// ----------------------------

type CreateUserInput struct {
	Email string
}

type CreateUserOutput struct {
	Id string
}

type UserRepository interface {
	Create(i *CreateUserInput) (*CreateUserOutput, error)
}
```

On our user model, we only have the [contract](#contracts-and-implementations) for the [Repository](#repositories).

```go
// internal/models/auth.go

package models

// ----------------------------
//
//      UseCase
//
// ----------------------------

type CreateFromEmailInput struct {
	Email string `json:"email" validate:"required,email"`
}

type AuthOutput struct {
	UserId string `json:"userId"`
}

type AuthUsecase interface {
	CreateFromEmailProvider(i *CreateFromEmailInput) error
}
```

And on our auth model, we only have the [contract](#contracts-and-implementations) for the [UseCase](#usecase).

We could have done it in only 1 model, but I choose to split it in to models to explain to you guys the multiple ways that models can be used.

### Adapter Example

![Adapter Example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5q05pbyelhwa66ope38t.png)

You can see that we have 2 main components here:
- An `implementations` folder
- And an `id.go` file

The `id.go` is the [Contract](#contracts-and-implementations) for the adapter. Unlike [Repositories](#repositories) and [UseCases](#usecases), the contracts for the [Adapters](#adapters) are grouped in the adapters folder and not in the [Models](#models).

And inside the `implementations` folder we have the real implementations for the contracts.

```go
// internal/adapters/id.go

package adapters

type IdAdapter interface {
	GenId() (string, error)
}
```

On the `id.go`, you can see that we define a [Contract](#contracts-and-implementations) for a adapter that generates an ID. you can see that the contract doesn't care if the ID is an UUID, ULID, number, or how it's generated, it only cares that the ID must be an string.

```go
// internal/adapters/implementations/ulid/ulid.go

package ulid

import (
	"github.com/oklog/ulid/v2"
)

type Ulid struct {
}

func (adp *Ulid) GenId() (string, error) {
	return ulid.Make().String(), nil
}
```

On the `implementations/ulid/ulid.go` is where we implement the contract, generating the ID, here we use the `oklog/ulid` library to generate an ULID.

You can see that the folder and file names are relative to how they are implementing it (using ULID) and not the [Contract](#contracts-and-implementations). This is because we can have multiple types of implementations for the same contract, like having both `implementations/ulid/ulid.go` and `implementations/uuid/uuid.go` implementing the `IdAdapter`.

### Repository Example

![Repository Example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zab7y23b3xeoc95jllhp.png)

here we have only the implementations directly, because the [Contracts](#contracts-and-implementations) are defined in the [Models](#models).

```go
// internal/repositories/user.go

package repositories

import (
	"database/sql"
	"errors"

	"example/internal/adapters"
	"example/internal/models"
)

type UserRepository struct {
	Db *sql.DB

	IdAdapter adapters.IdAdapter
}

func (rep *UserRepository) Create(i *models.CreateUserInput) (*models.CreateUserOutput, error) {
	accountId, err := rep.IdAdapter.GenId()
	if err != nil {
		return nil, errors.New("fail to generate id")
	}

	_, err = rep.Db.Exec(
		"INSERT INTO users (id, email) VALUES ($1)",
		accountId,
		i.Email,
	)
	if err != nil {
		return nil, errors.New("fail to create account")
	}

	return &models.CreateUserOutput{
		Id: accountId,
	}, nil
}
```

### Usecase Example

```go
// internal/usecases/user.go

package usecases

import (
	"errors"

	"example/internal/models"
)

type AuthUsecase struct {
	UserRepository models.UserRepository
}

func (serv *AuthUsecase) CreateFromEmailProvider(i *models.CreateFromEmailInput) error {
	_, err := serv.UserRepository.Create(&models.CreateUserInput{
		Email: i.Email,
	})
	if err != nil {
		return errors.New("fail to create user")
	}

	return nil
}
```

We can see that the Usecase receives the `UserRepository` as a [dependency injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/), and then uses it to  create an user.

This is a very simplistic version of a usecase, but in here you cold do thing like:
- Check if there are any other user with the same email (using a Repository) and return an error
- Send a welcome email (using an Adapter)

### Validators Examples

In our case, we already implemented the validators!

```go
// internal/models/auth.go

// ...

type CreateFromEmailInput struct {
	Email string `json:"email" validate:"required,email"`
}

// ...
```

In the Golang implementation, the input for the usecase already is the validator. Here we are using [go-playground/validator](https://github.com/go-playground/validator) to do this validations, and the implementation for this is simply usign the tag `validate`.

In other implementations, you may want to create a folder `delivery/dtos` and put your validations there, grouped by domain, or any other scenario that fits best your case.

### Delivery Example (HTTP)

![Delivery Example (HTTP)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ion2n93lbfq6o4in5ity.png)

In the delivery layer, we have a folder for each delivery type (http, cron, queues, etc) and a `validator.go` file to configure the validator. I'll not show you the content of `validator.go` here because it doesn't matter for the architecture concept, but you can give a look at the repository on the end of this article do see a more detailed implementation.

```go
// internal/delivery/http/index.go

package http

import (
	"encoding/json"
	"net/http"
	"os"

	"example/internal/delivery"
	"example/internal/models"
	"example/internal/utils"
)

func NewHttpDelivery(authUsecase models.AuthUsecase) {
	router := http.NewServeMux()
	validator := delivery.NewValidator()

	server := &http.Server{
		Addr:    ":" + os.Getenv("PORT"),
		Handler: router,
	}

	router.HandleFunc("POST /auth/email", func(w http.ResponseWriter, r *http.Request) {
		body := &models.CreateFromEmailInput{}
		err := json.NewDecoder(r.Body).Decode(body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		err = validator.Validate(body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		err = authUsecase.CreateFromEmailProvider(body)
		if err != nil {
			http.Error(w, err.Error(), err.(*utils.HttpError).HttpStatusCode())
			return
		}
	})

	server.ListenAndServe()
}

```

On the delivery implementation, it receives the AuthUseCase and uses it, validating the input before sending to the usecase.

### Putting everything together

```go
// main.go

package main

import (
	"database/sql"
	"os"

	"example/adapters/implementations/ulid"
	"example/delivery/http"
	"example/repositories"
	"example/usecases"
	_ "github.com/lib/pq"
)

func main() {
	db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
	if err != nil {
		panic(1)
	}
	defer db.Close()

	// ----------------------------
	//
	// Adapters
	//
	// ----------------------------

	ulidAdapter := &ulid.Ulid{}

	// ----------------------------
	//
	// Repositories
	//
	// ----------------------------

	userRepository := &repositories.UserRepository{
		Db: db,

		IdAdapter: ulidAdapter,
	}

	// ----------------------------
	//
	// Services
	//
	// ----------------------------

	authUsecase := &usecases.AuthUsecase{
		UserRepository: userRepository,
	}

	// ----------------------------
	//
	// Delivery
	//
	// ----------------------------

	http.NewHttpDelivery(authUsecase)
}
```

On the main file of your system, you create an instance of every adapter, repository and usecase, and then use the usecases on your delivery layer.

## Conclusion

Thanks for reading everything and feel free to share your thoughts on the comments to try to improve this architecture.

If you want a more detailed example (but that is kinda incomplete in some parts) you can check [this repository](https://github.com/henriqueleite42/authentication).