Clean Architecture Microservices Design

When designing services from scratch, its quite important to lay good foundations on which others can build the features/modules on. This article will discuss some of them. I will take example of Go but it can be followed in language agnostic manner.

  • Clean Architecture

The code is split into 4 core layers:

[Yellow] Domain Layer (1st layer)

Handles the entities

[Red] Application Layer (2nd layer)

Handles the use-cases

[Green] Adapter Layer ( 3rd layer)

Handles controllers/gateways/presenter

[Blue] External Layer (4th layer)

Handles external interfaces

Clean Architecture Diagram

In clean architecture, the dependencies only points inwards as can be seen with "-->"\ So in above case the Dependency Injection will happen with the flow:

  1. Define Interface

  2. Take argument as interface and call functions to it

  3. Implement those interfaces

  4. Inject dependency

Above provides us Dependency Inversion (D in S.O.L.I.D) i.e. one should depend upon abstractions and not concrete types. This also leads to a test friendly code as you can pass the mock respecting the same interface without the core logic to change.

So in terms of Go, Instead of depending on a concrete implementation (class, struct), we depend on interfaces (protocol) and all dependencies should be given from outside.

So in terms of DI roles are defined as

Client (producer): implementing the logic/class

Service (consumer): you depend on some class

Interface(bridge): bridge to connect above two.

Fig:

key is to pass the service to the client, rather than allowing a client to build or find the service.

.
└── internal
    └── app
        ├── adapter
        │   ├── controller.go    # 4. Dependency Injection (client interacts to this)
        │   └── repository
        │       └── ticketsRepo.go # 3. Core implementation
        ├── application
        │   └── usecase
        │       └── ticketsUseCase.go # 2. Interface Function Call
        └── domain
            ├── entity.go
            └── repository
                └── ticketsEntity.go # 1. Interface

Let's look at the Code taking Go as example going from outwards to inwards as per above diagram.

// controller.go a.k.a place where DI happens.

package adapter

import (
    "somerepopath/internal/app/adapter/repository"
    "somerepopath/internal/app/application/usecase"
)

var (
    trepo = repository.TicketsRepo{}
    // here add deps your usecases needs ex:
    // - some other repos etc
)

func (ctrl Controller) parameter(c interface{}) {
    parameter := usecase.Parameter(trepo) // Dependency Injection
    c.JSON(200, parameter)
}
// ticketsRepo.go

package repository

// TicketsRepo is the repository of domain.Ticket
type TicketsRepo struct{}

// Get gets ticket
func (r TicketsRepo) Get() (*domain.Ticket, error) {
    db := someDB.Connection()
    var m model.Ticket
    result := db.First(&m, 1)
    if result.Error != nil {
        return nil, result.Error
    }
    return domain.Ticket{
        TicketID: result.ID,
        Content:  result.Content,
    }, nil
}
// ticketsUseCase.go

package usecase

// NOTICE: This usecase DON'T depend on Adapter layer
import (
    "somerepopath/internal/app/domain"
    "somerepopath/internal/app/domain/repository"
)

// Ticket is the usecase of getting a ticket
func Ticket(r repository.TicketsRepo) (domain.Ticket, error) {
    return r.Get()
}
//ticketsIface.go

package repository

import "somerepopath/internal/app/domain"

// TicketsIface is interface of tickets repository
type TicketsIface interface {
    Get() (domain.Ticket, error)
}

// entity.go

type Ticket struct {
  ID string `json:"ticket_id"`
  Content string `json:"content"`
}

Take notice that adapter being the out most layer it actually calls the usecase module and usecase module doesn't even know how to get called or how to create the input params(ticketsRepo) to operate i.e typical Dependency Injection.

Also take note that we do manual DI, In go there are various libraries created to handle this like wire, fx.

Our External layer(mostly containing routing logic handlers) will call the controllers (adapters) to trigger various usercases.

That's it! Unidirectional flow of control with clear laid out plan.

Thanks!