Using Clean Architecture in Go

One of the problems that I often encounter in various software projects is the strong coupling of the code, which makes it so difficult to make even simple changes to it without causing unwanted side effects. The fact is that programmers tend to focus on developing specific features without thinking about how the code base will develop in the future. Also, not everyone takes into account that the libraries and frameworks used today may gradually disappear from the scene after a few months or years.

At the start of a project, many decisions have to be made. Most engineers consider the scope of the project and decide which tools will be used to implement it. We are talking, in particular, about programming languages, frameworks, databases, external APIs, and deployment options. By making such decisions at the earliest stages, they become locked into these tools, permeating the entire code base with them, as a result of which it becomes difficult to change and maintain.

Most of these tools are specific, and the choice of most of them (except the programming language) can be postponed for a while until the project is more mature. Therefore, in the early stages of project development, it is worth paying attention not to what tools will be used for implementation. It is better to model the project domain, and approach the above-mentioned tools as they should be – that is, as particulars. Of course, for the project to be implemented, such details also need to be decided, but they can remain in some separate part of the code that is not related to the subject area – where they can be easily changed, deleted or replaced at our discretion.

To solve precisely such problems with strong code coherence, highly experienced engineers have created a number of architectural patterns. These are, in particular, clean architecture Robert Martin (“Uncle Bob”) hexagonal architecture Alistair Cockburn and explicit architecture Herberto Grazi.

This article will show how I was able to build this blog using concepts related to these systems. I mostly stuck to a clean architecture, but also borrowed some concepts and naming principles from other systems. All code is written in Go, but the ideas discussed here apply to any language.

Layers

Layers are divided into four main categories: Core, Adapters, User Interface, and Main. Each of them is explained below.

Core

This is the core of the application, and it contains exclusively the business logic rules. All components of this layer do not have any external dependencies. All tests are run without running any web or database server.

The core layer is divided into core components. At the level of these components, it is convenient to divide the application into independent parts, each of which can be developed separately. In my project there is only one nuclear component, it is called blog. Examples of other components also classified as nuclear are: comment, authentication or authorization.

Within each of the nuclear components there are 3 types of elements:

All kernels exhibit this type of behavior:

use case

called from another layer that receives some data, after which some data is loaded from long-term storage

essence

and this is done using

port

. The code then operates on the entity generating its new state. Other ports and entities may be used in this process. In the end, the entity is again long-term stored in the use case using

port

after which the expected result is returned.

Adapters

Each adapter implements a port interface used by the kernel. Any external dependency, be it a library, framework, external API or database, goes through the adapter. The adapter is a low-level component controlled at the high-level policy level (use case). This structure allows you to provide multiple adapters for the same port. You can also extend the behavior of the adapter using design patterns such as Composite or Decorator.

User interface

It is in the UI layer that the user interacts with the program. This layer can contain many content delivery tools, for example, website (HTML), REST API, GraphQL, gRPC, CLI. Each of them is a separate component in the UI layer.

Main layer (application)

It is in the “main” layer that all connections occur. This layer knows everything about other layers, including how to initialize all components with the correct dependencies. Because the package name

main

reserved in Go, in the blog implementation considered below this layer will be called

app

.

Blog project

The blog project is structured as follows. Posts are written in the form of markup (format

Markdown

) and are saved as text files in the directory

/posts

. Whenever a specific post is requested from a web server, a file containing that post is downloaded and then parsed. The content of the markup is then converted to HTML and the result of this operation is returned to the user.

The post file format is:

title: Заголовок поста

author: Имя автора

description: описание поста

image_path: /static/image/sample-image.jpg

time: год-месяц-день часов:минут

--

## Содержимое разметки

Implementation

This project was implemented using the Test-Driven Development methodology, but in order to keep this article simple, the tests themselves are omitted, as well as some code fragments. The full source code for the post is posted at

this repository

on GitHub.

Later in this post we will look at the implementation of the use case. view post across all layers. Other use cases are structurally similar, and all the code for them is posted in a repository on GitHub.

Directory structure

For this project, I'm following the standard directory structure found in the Go community. This structure is posted in the repository

project layout

on GitHub.

Let's look at the directories separately:

  • cmd – here is the main file of the content delivery mechanism (web).
  • deployment – deployment scripts.
  • internal – the most important directory, it contains implementations of all layers and components.
  • pkg – packages independent of the project. They are needed to “expand” the language. Each of these packages can be released as a standalone library.
  • posts – a directory specific to this project. Written posts are stored here, including markup for them.
  • test – contains integration tests and other testing resources.
  • web – material for presentation on the Web, in particular templates and static files.

Core

I always start implementing any feature by developing a user case. Use cases are laid at the core level, inside the components to which they belong. Since our application is quite small, there is only one such component –

blog

.

The user case always implements a high-level policy that describes how to work with the feature, and low-level details are left to the adapters connected to the ports. Each use case is presented in the format of a structure (class) with a method Run. The structure contains the dependencies and the request arguments are passed directly to the method Run. Here is the implementation blog.ViewPostUseCase:

// internal/blog/view_post_use_case.go

package blog

type ViewPostUseCase struct {
  postRepo PostRepo
  renderer Renderer
}

func NewViewPostUseCase(postRepo PostRepo, renderer Renderer) *ViewPostUseCase {
  return &ViewPostUseCase{postRepo: postRepo, renderer: renderer}
}

func (u *ViewPostUseCase) Run(path string) (RenderedPost, error) {
  post, err := u.postRepo.GetPostByPath(path)

  if err != nil {
    return RenderedPost{}, err
  }

  return u.renderPost(post)
}

func (u *ViewPostUseCase) renderPost(post Post) (RenderedPost, error) {
  renderedContent, err := u.renderer.Render(post.Markdown)

  if err != nil {
    return RenderedPost{}, err
  }

  return RenderedPost{
    Post: post,
    HTML: renderedContent,
  }, nil
}

Method

Run

receives the path to the post as an argument. Based on this path it downloads

Post

(entity) of

PostRepo

(port). If such a post is found, it displays the markup of that post using

Renderer

(port). Finally, he collects and returns

RenderedPost

(entity).

Post is a simple data structure that contains the post information returned PostRepo. There is another data structure RenderedPost, built inside a use case. It contains the original post and the displayed HTML in the form in which it should be submitted to the UI.

Ports PostRepo And Renderer – these are interfaces to which adapters can be connected. Now you can assemble the entire use case.

Here's how ports and entities are implemented:

// internal/blog/entities.go

package blog

import "time"

type Post struct {
  Title       string
  Author      string
  Time        time.Time
  Path        string
  Description string
  ImagePath   string
  Markdown    string
}

type RenderedPost struct {
  Post Post
  HTML string
}
// internal/blog/ports.go

package blog

import "errors"

type PostRepo interface {
  GetPostByPath(path string) (Post, error)
}

var ErrPostNotFound = errors.New("post not found")

type Renderer interface {
  Render(content string) (string, error)
}

Adapters

Adapters are low-level parts that are controlled from use cases. They are stored in the following directory structure:

internal/adapters/PORT/ADAPTER_TYPE

Where

PORT

is the name of the port implemented by the adapter, and

ADAPTER_TYPE

is the adapter type for this port. In this case we have an adapter

FileSystem

for port

PostRepo

and adapter

Goldmark

for the Renderer port, moreover, the latter is named after the Markdown library that it “adapts”.

Beginning with PostRepothe implementation is:

// internal/adapters/postrepo/filesystem/post_repo.go

package filesystem

import (
  "io/ioutil"
  "path/filepath"

  "github.com/geisonbiazus/blog/internal/core/blog"
)

type PostRepo struct {
  BasePath string
}

func NewPostRepo(basePath string) *PostRepo {
  return &PostRepo{BasePath: basePath}
}

func (r *PostRepo) GetPostByPath(path string) (blog.Post, error) {
  content, err := ioutil.ReadFile(filepath.Join(r.BasePath, path+".md"))

  if err != nil {
    return blog.Post{}, blog.ErrPostNotFound
  }

  post, err := ParseFileContent(string(content))
  post.Path = path

  return post, err
}

IN

filesystem.PostRepo

interface is implemented

blog.PostRepo

.

GetPostByPath

gets the path, in fact, the identifier of the post in the system, then reads the file that it finds in the repository constructor, following this path, and then parses the syntax of this file. This is done using the function

ParseFileContent

generating the entity

blog.Post

.

For simplicity, I do not provide the implementation of the function here ParseFileContent. With her implementationas well as with its corresponding tests you can find it on GitHub.

Second adapter used in ViewPostUseCase – This Renderer (see implementation below):

// internal/adapters/renderer/goldmark/renderer.go

package goldmark

import (
  "bytes"

  "github.com/alecthomas/chroma/formatters/html"
  "github.com/yuin/goldmark"
  highlighting "github.com/yuin/goldmark-highlighting"
)

type Renderer struct{}

func NewRenderer() *Renderer {
  return &Renderer{}
}

func (r *Renderer) Render(content string) (string, error) {
  var buf bytes.Buffer

  markdown := goldmark.New(
    goldmark.WithExtensions(
      highlighting.NewHighlighting(
        highlighting.WithStyle("monokai"),
        highlighting.WithFormatOptions(
          html.TabWidth(2),
        ),
      ),
    ),
  )

  err := markdown.Convert([]byte(content), &buf)

  if err != nil {
    return "", err
  }

  return buf.String(), nil
}

As with the post repository, the structure

goldmark.Renderer

implements the interface

blog.Renderer

. With its help, we abstract from the user case how exactly the markup is converted into HTML. The library is used for this

goldmark

.

Please note: this and only this file uses and mentions the library at all goldmark. Therefore, it becomes easier to expand the behavior of this library or even replace it as the project develops. This is where adapters are truly useful.

User interface

This blog uses HTTP and HTML technologies to deliver content. Therefore, I will call this component “website”

web

. This package includes everything related to working on the web, in particular, the server, router and handlers (in other languages ​​and frameworks they may be called “controllers”).

Let's start with the handler. Each handler, in order to get the result, first calls the use case from the kernel layer. To simplify testability, each use case is represented as a port in a package web, roughly the way the adapters are presented in the kernel package. Here, the implementation of the use case is the “adapter” of a given port in the UI layer. The use case port is defined as follows:

// internal/ui/web/ports.go

package web

import "github.com/geisonbiazus/blog/internal/core/blog"

type ViewPostUseCase interface {
  Run(path string) (blog.RenderedPost, error)
}

In other languages, such as Java or C#, which require an explicit mention of the interface being implemented, this port would be defined within the kernel layer. This way they become functional without knowing anything about the outer layers. This way we don't create circular dependencies. But because Go implements interfaces implicitly, it is possible to keep an interface close to where it is used without causing too much coupling.

Having arranged the port for the use case, we move on to the implementation of the handler, which is done like this:

// internal/ui/web/view_post_handler.go

package web

import (
  "fmt"
  "html/template"
  "net/http"
  "path"

  "github.com/geisonbiazus/blog/internal/core/blog"
)

type ViewPostHandler struct {
  usecase  ViewPostUseCase
  template *TemplateRenderer
}

func NewViewPostHandler(
  usecase ViewPostUseCase, templateRenderer *TemplateRenderer,
) *ViewPostHandler {
  return &ViewPostHandler{
    usecase:  usecase,
    template: templateRenderer,
  }
}

func (h *ViewPostHandler) ServeHTTP(
  res http.ResponseWriter, req *http.Request,
) {
  path := path.Base(req.URL.Path)
  renderedPost, err := h.usecase.Run(path)

  switch err {
  case nil:
    res.WriteHeader(http.StatusOK)
    h.template.Render(res, "view_post.html", h.toViewModel(renderedPost))
  case blog.ErrPostNotFound:
    res.WriteHeader(http.StatusNotFound)
    h.template.Render(res, "404.html", nil)
  default:
    res.WriteHeader(http.StatusInternalServerError)
    h.template.Render(res, "500.html", nil)
  }
}

func (h *ViewPostHandler) toViewModel(p blog.RenderedPost) postViewModel {
  return postViewModel{
    Title:       p.Post.Title,
    Author:      p.Post.Author,
    Description: p.Post.Description,
    ImagePath:   p.Post.ImagePath,
    Path:        fmt.Sprintf("/posts/%s", p.Post.Path),
    Date:        p.Post.Time.Format(DateFormat),
    Content:     template.HTML(p.HTML),
  }
}

type postViewModel struct {
  Title       string
  Author      string
  Date        string
  Description string
  ImagePath   string
  Path        string
  Content     template.HTML
}

Structure

web.ViewPostHandler

implements the Go internal interface

http.Handler

, which is used by default to handle HTTP requests using the standard library. Method

ServeHTTP

extracts the path to the post from the request, and then executes the use case and at the same time receives

blog.RenderedPost

and the possible result of the error. Depending on the result of the error, the corresponding status code is set, and then using the module

web.TemplateRenderer

the template is displayed.

If successful, it generates postViewModel. This view model contains all the post information in a properly formatted form, ready to be displayed in the user interface. The view model is then passed to the template renderer, which will simply generate the resulting HTML and display it to the user.

Template handler, router And server are also part of the web package, but a story about their implementation is beyond the scope of this post. Their code can be viewed on GitHub.

Application

Once the implementations of all these layers and components are ready, all that remains is to put everything together. These connections are made in the app layer. This component knows about all other components and understands what dependencies are associated with each component. Here's the implementation:

// internal/app/context.go

package app

import (
  "log"
  "net/http"
  "os"
  "path/filepath"

  "github.com/geisonbiazus/blog/internal/adapters/postrepo/filesystem"
  "github.com/geisonbiazus/blog/internal/adapters/renderer/goldmark"
  "github.com/geisonbiazus/blog/internal/core/blog"
  "github.com/geisonbiazus/blog/internal/ui/web"
  "github.com/geisonbiazus/blog/pkg/env"
)

type Context struct {
  Port         int
  TemplatePath string
  StaticPath   string
  PostPath     string
  BaseURL      string
}

func NewContext() *Context {
  return &Context{
    Port:         env.GetInt("PORT", 3000),
    TemplatePath: env.GetString("TEMPLATE_PATH", filepath.Join("web", "template")),
    StaticPath:   env.GetString("STATIC_PATH", filepath.Join("web", "static")),
    PostPath:     env.GetString("POST_PATH", filepath.Join("posts")),
    BaseURL:      env.GetString("BASE_URL", "http://localhost:3000"),
  }
}

func (c *Context) WebServer() *web.Server {
  return web.NewServer(c.Port, c.Router(), c.Logger())
}

func (c *Context) Router() http.Handler {
  return web.NewRouter(c.TemplatePath, c.StaticPath, c.UseCases(), c.BaseURL)
}

func (c *Context) UseCases() *web.UseCases {
  return &web.UseCases{
    ViewPost:  c.ViewPostUseCase(),
  }
}

func (c *Context) ViewPostUseCase() *blog.ViewPostUseCase {
  return blog.NewViewPostUseCase(c.PostRepo(), c.Renderer())
}

func (c *Context) PostRepo() *filesystem.PostRepo {
  return filesystem.NewPostRepo(c.PostPath)
}

func (c *Context) Renderer() *goldmark.Renderer {
  return goldmark.NewRenderer()
}

func (c *Context) Logger() *log.Logger {
  return log.New(os.Stdout, "web: ", log.Ldate|log.Ltime|log.LUTC)
}

Module

app.Context

acts as a container for dependency injection. For example, if it is asked for a web server, it will be able to correctly assemble all the dependencies from all layers to make the web server work. The same goes for any other application component.

Main file

Let's move on, for example, to the main file. The implementation is quite simple, because this is just the entry point for the application:

// cmd/web/main.go

package main

import (
  "log"

  "github.com/geisonbiazus/blog/internal/app"
)

func main() {
  c := app.NewContext()
  log.Fatal(c.WebServer().Start())
}

Function

main

just creates

app.Context

gets the web server and starts it.

Conclusion

According to Robert Martin, good architecture maximizes the number of decisions that

don't have to take it

. That's the point. If every single low-level detail of your application is implemented as an adapter, then you don't feel pressured to make the right decision at the very beginning. Indeed, in reality, the right decision always depends on when exactly the implementation is made.

It's likely that a component that works today will no longer be the best option in a few months or years. Sometimes, in order to meet deadlines, we cut corners and deliberately increase technical debt. This is why it is absolutely necessary to control this aspect in good architecture.

Clean architecture concepts greatly simplify software development and maintainability. You don't have to waste time deciding early on what your infrastructure will be like. If you need to change something, then (provided that the business logic rules do not change), you just need to implement a new adapter. If you need to change a business logic rule, then minimal changes are made to the adapters, sometimes no changes are required at all.

That's why this approach is so good. Everything is easy to change, easy to test, everything is in its place in the code base.


You might also want to read this:

News, product reviews and competitions from the Timeweb.Cloud team – in our Telegram channel

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *