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
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:
Define Interface
Take argument as interface and call functions to it
Implement those interfaces
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!