Why is Go-Kit Perfect For Clean Architecture in Golang?

Oren Rose
9 min readSep 17, 2023

--

Introduction

Go is perfect for Clean Architecture. Clean Architecture alone is just an approach, it doesn’t tell you how to structure your code source. It is very significant to realize it when trying to implement it in a new language.

It took me several tries to write my first service since I’ve had experience with Ruby on Rails, and most articles I’ve read about clean Architecture in Go described the structure layout in a non-Go idiomatic way. Part of the smell was that the packages in these examples were named according to the layer — controllers, models, services, etc… If you have these kinds of packages, it’s the first red flag telling you that your application requires a redesign. In Go, package names should describe what the package provides, not what it contains.

Then I got to know go-kit, especially the shipping example, and decided to implement the same structure in my application. Later when I dived deeper into Clean Architecture, I was pleasantly surprised to find out how go-kit approach is perfect.

In this post, I will describe how writing services using the Go-Kit approach very much corresponds to the clean architecture idea.

Clean Architecture

Clean Architecture is a software architecture design that was created by Robert Martin (Uncle Bob). Its objective is the separation of concerns, allowing developers to encapsulate the business logic and keep it independent from the delivery and framework mechanism. The same goal is also shared by many other architecture paradigms such as Onion and Hexagon Architectures. They all achieve this separation by dividing the software into layers.

The arrows in the circles show the dependency rule. If something is declared in an outer circle, it must not be mentioned in the inner circle code. It goes both for actual source code dependencies and for naming. Inner layers are not dependent on any outer layer.

The outer layer contains the low-level components such as UI, DB, transport, or any 3rd party service. They all can be thought of as details or plugins to the application. The idea is that a change in an outer layer must not cause any change in the inner layers.

The dependency between the different modules/components can be described as the following:

Note that the arrows crossing the boundaries are only pointing one way, The components behind the boundary belong to the outer layers. The controller, presenter, and the database. The Interactor is where the BL is implemented and can be thought of as the use-case layer.

Notice the Request Model and the Response model . These are objects describing respectively the data that the inner layer requires and returns. The controller translates the request (HTTP request in the case of the web), into the Request Model, and the presenter formats the Response Model into data that can be presented by the View Model.

Also notice the interfaces, which are used for inverting the flow of control to correspond with the dependency rule. The Interactor speaks with the presentervia Boundary interface, and with the data layer via Entity Gateway interface.

This is the main idea of clean architecture. Separate the layers by dependency injection. Invert the flow of control with dependency inversion. The Interactor (BL) and the entities know nothing about the transport and the data layer. This is important for not having cascading changes in our inner layers if we change a detail in the outer ones.

What is Go-Kit?

Go kit is a collection of packages that help you build robust, reliable, and maintainable microservices.

The important thing for me, coming from Ruby on Rails, is that Go-Kit is not MVC framework. Instead, it divides the application into three layers:

  • Transport
  • Endpoint
  • Service

Transport

The transport is the only component that is familiar with the delivery mechanism (HTTP, gRPC, CLI…). This is powerful since you could support both HTTP and CLI by only providing a different transport.

Later we’ll see how the transport corresponds to the controller and presenter in the above diagram.

Endpoint

type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)

An endpoint represents a single RPC in your application. It connects the delivery to your BL. This is where you actually define the use case in terms of input and output. In clean-architecture terminology — Request Model and Response Model.

Note that the endpoint is a function that receives a request and returns a response, both of them are interface{} , these are the RequestModel and the ResponseModel. Theoratically it also can be implemented with type parameters (generics).

Service

The service(interactor) is where the BL is implemented. The service has no knowledge of the endpoint, and neither the service nor the endpoint has any knowledge of the transport domain, such as HTTP

Go-Kit provides a function for creating the server (HTTP server/gRPC server, etc.). For example, in HTTP:

package http // under go-kit/kit/transport/http

type DecodeRequestFunc func(context.Context, *http.Request) (request interface{}, err error)
type EncodeResponseFunc func(context.Context, http.ResponseWriter, interface{}) error

func NewServer(
e endpoint.Endpoint,
dec DecodeRequestFunc,
enc EncodeResponseFunc,
options ...ServerOption,
) *Server
  • DecodeRequestFunc translates the HTTP request to the Request Model, and
  • EncodeResponseFunc formats the Response Model and encodes it in the HTTP response.
  • The returned *server implements http.Server (has ServeHTTP method).

The transport uses this function in order to create the http.Server. The decoder and encoder are defined in the transport, and the endpoint is initialized on run-time.

Short example: (based on the shipping example)

Our Simple Service

We will describe a simple service with two APIs, for Creating and Reading an article from the data layer. The transport is HTTP, and the data layer is just an in-memory map. You can find the GitHub source code here.

Notice the file structure:

- inmem
- articlerepo.go
- publishing
- transport.go
- endpoint.go
- service.go
- formatter.go
- article
- article.go

Let’s see how these represent the different layers of clean architecture

  • article — this is the entity layer, containing no knowledge of the BL, the data layer, or the transport.
  • inmem — this is the data layer.
  • transport — this is the transport layer.
  • endpoint + service — both of them compose the Boundary + Interactor.

Starting with the service:

import (
"context"
"fmt"
"math/rand"

"github.com/OrenRosen/gokit-example/article"
)

type ArticlesRepository interface {
GetArticle(ctx context.Context, id string) (article.Article, error)
InsertArticle(ctx context.Context, thing article.Article) error
}

type service struct {
repo ArticlesRepository
}

func NewService(repo ArticlesRepository) *service {
return &service{
repo: repo,
}
}

func (s *service) GetArticle(ctx context.Context, id string) (article.Article, error) {
return s.repo.GetArticle(ctx, id)
}

func (s *service) CreateArticle(ctx context.Context, artcle article.Article) (id string, err error) {
artcle.ID = generateID()
if err := s.repo.InsertArticle(ctx, artcle); err != nil {
return "", fmt.Errorf("publishing.CreateArticle: %w", err)
}

return artcle.ID, nil
}

func generateID() string {
// code emitted
}

The service knows nothing about the delivery and the data layer — it doesn’t import anything from the outer layers (HTTP, inmem…). Here is where the BL resides. You might say that there isn’t really BL and the service here might be redundant, but need to remember that this is just a simple example.

The entity

package article

type Article struct {
ID string
Title string
Text string
}

Our entity is just a DTO. If we would have a business policy or behavior, we could add it here.

Endpoint

The endpoint.go defines the service interface:

type Service interface {
GetArticle(ctx context.Context, id string) (article.Article, error)
CreateArticle(ctx context.Context, thing article.Article) (id string, err error)
}

Then, it defines an endpoint for each use case (RPC). For example, for Get Article:

type GetArticleRequestModel struct {
ID string
}

type GetArticleResponseModel struct {
Article article.Article
}

func MakeEndpointGetArticle(s Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req, ok := request.(GetArticleRequestModel)
if !ok {
return nil, fmt.Errorf("MakeEndpointGetArticle failed cast request")
}

a, err := s.GetArticle(ctx, req.ID)
if err != nil {
return nil, fmt.Errorf("MakeEndpointGetArticle: %w", err)
}

return GetArticleResponseModel{
Article: a,
}, nil
}
}

Note how it defines a RequestModel and ResponseModel. This is the input/output of this RPC. The idea is that you can see the data that is required (input) and the data that is returned (output) without even reading the implementation of the endpoint itself. This is how I think of the endpoint representing a single RPC . The service has the methods for actually triggering the BL, but the endpoint defines the applicative definition of the RPC. Theoretically, an endpoint can trigger multiple BL methods.

Transport

The transport.go registers the HTTP routes:

type Router interface {
Handle(method, path string, handler http.Handler)
}

func RegisterRoutes(router *httprouter.Router, s Service) {
getArticleHandler := kithttp.NewServer(
MakeEndpointGetArticle(s),
decodeGetArticleRequest,
encodeGetArticleResponse,
)

createArticleHandler := kithttp.NewServer(
MakeEndpointCreateArticle(s),
decodeCreateArticleRequest,
encodeCreateArticleResponse,
)

router.Handler(http.MethodGet, "/articles/:id", getArticleHandler)
router.Handler(http.MethodPost, "/articles", createArticleHandler)
}

The transport uses the MakeEndpoint functions for creating the endpoint on runtime and provides a decoder for deserializing the request and an encoder for formatting and encoding the response.

For example:

func decodeGetArticleRequest(ctx context.Context, r *http.Request) (request interface{}, err error) {
params := httprouter.ParamsFromContext(ctx)
return GetArticleRequestModel{
ID: params.ByName("id"),
}, nil
}

func encodeGetArticleResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
res, ok := response.(GetArticleResponseModel)
if !ok {
return fmt.Errorf("encodeGetArticleResponse failed cast response")
}

formatted := formatGetArticleResponse(res)
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(formatted)
}

func formatGetArticleResponse(res GetArticleResponseModel) map[string]interface{} {
return map[string]interface{}{
"data": map[string]interface{}{
"article": map[string]interface{}{
"id": res.Article.ID,
"title": res.Article.Title,
"text": res.Article.Text,
},
},
}
}

You might ask yourself, why did I use another function for formatting the article and didn’t just add JSON tags on the article entity?

This is a very important question. Adding JSON tags on the article entity implies that the article knows how it is formatted. Although there isn’t an explicit import to HTTP, it breaks the abstraction and makes the entity package dependent on the transport layer.

For example, imagine you would like to change the response to the client from “title” to “header”. This change is only a concern for the transport layer. However, if this requirement causes you to change the entity, it means that the entity depends on the transport layer, which breaks Clean Architecture.

Putting it together:

Let’s see what a dependency diagram would look like like for our simple application:

Wow, you must note the similarity! the article entity has no dependencies (only inward arrows). The outer layers, transport and inmem, only have arrows towards the inner layers of the BL and the entity.

It’s all about translations

Crossing boundary is all about the translation between different layers’ languages.

The BL layer ONLY speaks the application language. That is, knows only about the entity. (nothing about HTTP requests or SQL queries). In order to cross the boundary, someone in the flow must translate the application language into the outer layer languages.

In the transport layer, we have the decoder, which translates the HTTP request into the application language of RequestModel, and the encoder, which translates the application language ResponseModel into HTTP response.

In the data layer, we have the implementation of the repo. In our case it’s inmem. In another case we will might have package sql responsible for translating the application language to SQL language (query and raw results)

Package “ing"

You might say that the transport and service shouldn’t be in the same package, since they are in different layers. This would be a correct argument. I’ve taken the example from the shipping example in go-kit which had this kind of design. Package ing containing the transport/endpoint/service which I found very convenient in the long run. With that said, it is possible that I would use a different package for the transport if I were writing it today.

Last word about “Screaming Architecture”

Another reason Go is perfect for Clean ARchitecture, is the package naming and ideology. Screaming Architecture is about structuring your application so that it is obvious what the intention of the application is. In Ruby On Rails, when looking at the structure, you know that it is written in Ruby On Rails framework (controllers, models, views…). In our application, when looking at the structure, you can tell that it is an application about articles, has publishing use cases, and uses an inmem data layer.

Summary

Clean Architecture is just an approach, it doesn’t tell you how to structure your code source. The art is to know the idiomatic way to do it in your language, with the conventions and tools it provides. I hope that you have found this post somehow helpful, the important thing to realize is, that posts in the wild that are arguing to have the correct solution for a design problem aren’t always telling the whole truth. Including this one :)

--

--

Oren Rose
Oren Rose

Written by Oren Rose

I’m a back-end developer, Golang enthusiastic and Clean Architecture zealous

Responses (6)