One of the problems I frequently see on many software projects is the coupling of the code and how hard it is to make simple changes without undesired side effects. This happens mainly because developers tend to focus on building the features without thinking about how the codebase will evolve in the future, and also without taking into consideration that the libraries and frameworks they are using today might not be the best option in some months or years.
At the beginning of a project, there are a lot of decisions that need to be made. And what many engineers do is look at the scope of the project and decide on which tools they will use to build it. Those tools are programming languages, frameworks, databases, external APIs, deployment, among others. By taking such decisions early on, they lock into these tools and couple the entire code base, making it hard to change and maintain.
Most of those tools are mere details and except for the programming language, all other decisions can be delayed for some time until the project gets more mature. Instead of deciding in which tools the project will be made, the effort, early in the project, should be on how the domain model will look like, and treat those tools as what they are, details. Of course, that to deliver the project, the details have to be chosen, but they can stay in a part of the code, separated from the domain, where they can be easily changed, removed, or replaced whenever we want.
It is exactly to solve this problem of coupling that highly experienced engineers created some architectural patterns such as the Clean Architecture from Robert "Uncle Bob" Martin, the Hexagonal Architecture from Alistair Cockburn, and the Explicit Architecture from Herberto Graça.
In this post, I show how I built this Blog using concepts from these patterns. I followed mainly the Clean Architecture but I also used some concepts and naming conventions from the other patterns. It is implemented using the Go programming language, but these ideas can be applied to any language.
There are four main kinds of layers: Core, Adapters, User Interface, and Main. You can see the explanation for each one below.
This is the core of the application and it holds only pure business rules. All of its components have no external dependencies. All tests pass without starting any web server or having started a database server.
The core layer is divided into core components. These components separate the application into independent developable parts. This project has only one core component called blog
. Other examples of core components could be comment
, authentication
, or authorization
.
Inside of each core component, there are 3 kinds of elements:
A common behavior found in the core is that a use case
is called by another layer receiving some data, then it loads some entity
from the persistence using a port
. After that, it manipulates the entity generating its new state. It can use other ports or entities in this process. In the end, the use case persists the entity again using a port
and returns the expected result.
Each adapter implements a port interface used by the core. Any external dependency, that being a library, a framework, an external API, or a database, goes to an adapter. The adapter is the low-level detail that the high-level policy (use case) manipulates. This structure allows us to have multiple adapters for the same port, it also allows us to extend the behavior of the adapter by using design patterns like Composite or Decorator.
The UI layer is where the interaction of the user goes. Inside of this layer, you can have multiple delivery mechanisms like Website (HTML), REST API, GraphQL, gRPC, CLI, each one being a different component inside of the UI layer.
The "Main" layer is where all the wiring goes. This layer knows everything about the other layers and knows how to initialize all components with the correct dependencies. Due to the main
package being a reserved package name in Go, this layer was called app
in the blog implementation.
The way the Blog project works is the following: Posts are written in the Markdown format and are stored as text files in the /posts
directory. Whenever a request for a specific post comes through the web server, the post file is loaded and parsed, the Markdown content is converted to HTML, then the result is returned to the user.
The post file has the following format:
title: Post title
author: Author name
description: Post description
image_path: /static/image/sample-image.jpg
time: 2021-04-21 22:37
--
## Markdown Content
The project has been implemented using Test Driven Development but the tests as well as some parts of the code are omitted from this post for simplicity. You can find the full source code on the blog repository on GitHub.
In this post, I show you the implementation of the View Post use case through all the layers. Other use cases follow a similar structure and the code can be seen in the GitHub repository.
The project follows a standard folder structure from the Go community. This structure can be found in the project layout repository on GitHub.
The current folders are:
Whenever I start implementing any feature, I always start with a use case. The use cases go in the core layer inside of the components they belong. As this application is small enough there is only one component called blog
.
The use case always implements the high-level policy of the feature leaving the low-level details for the adapters plugged in the ports. Every use case has the format of a struct (class) with a Run
method. The struct holds the dependencies while the request arguments are passed directly in the Run
method. Here is the implementation of the 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 }
The Run
method receives a post path as an argument. With this path, it loads the Post
(entity) from the PostRepo
(port). If the post is found it renders the post Markdown using the Renderer
(port). Finally, it builds and returns a RenderedPost
(entity).
Post
is a simple data structure containing the post data returned by the PostRepo
while RenderedPost
is another data structure built inside the use case containing the original Post and the rendered HTML to be returned to the UI.
The ports PostRepo
and Renderer
are interfaces where adapters can be plugged in to build the full use case flow.
Here is how the 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 are the low-level details controlled by the use cases. They are stored in the following directory structure: internal/adapters/PORT/ADAPTER_TYPE
where PORT
is the name of the port it implements and ADAPTER_TYPE
is the type of adapter for that port. In this case, we have a FileSystem
adapter for the PostRepo
port and a Goldmark
adapter for the Renderer
port, the last being named after the name Markdown library it "adapts".
Starting with the PostRepo
, the implementation is the following:
// 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 }
The filesystem.PostRepo
implements the blog.PostRepo
interface. The GetPostByPath
receives a path, which is the post identifier in the system, reads the file based on the base path given in the repository constructor, and then parses the file content using the ParseFileContent
function generating a blog.Post
entity.
The ParseFileContent
function implementation is omitted from this post for simplicity. You can check its implementation as well as its tests on GitHub.
The second adapter used by the ViewPostUseCase
is the Renderer
and you can see the implementation next:
// 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 }
Similar to the post repository, the goldmark.Renderer
struct implements the blog.Renderer
interface. It abstracts away from the use case of how the markdown is converted to HTML. The goldmark library is used for that.
Note that this file is the only place that the goldmark
library is used or mentioned. This makes extending the behavior or replacing the library simpler when evolving the project. This is the great benefit of the adapters.
The blog project uses HTTP and HTML as its delivery mechanism. For this, I chose the name web
as the name of this "website" component. Everything web-related like a server, router, and handlers (also known as controllers in other languages or frameworks) goes to this package.
Starting with the handler, each handler calls a use case from the core layer to get its result. For better testability, each use case is represented as a port inside of the web
package similar to the adapters in the core package. Here, the use case implementation is the "adapter" of this 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# where you need to explicitly mention the implemented interface, this port would be defined inside of the core layer to avoid it from knowing the outer layers creating cyclic dependencies. But as Go interfaces are implicitly implemented, it is possible to keep this interface closer to where it is used without introducing coupling.
With the use case port in place, the handler implementation is done as follows:
// 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 }
The web.ViewPostHandler
struct implements the Go internal http.Handler
interface which is the standard to handle HTTP requests using the standard library. The ServeHTTP
method extracts the post path from the request and then runs the use case getting a blog.RenderedPost
and a possible error result. Based on the error result it sets the appropriate status code and renders a template using the web.TemplateRenderer
module.
In case of success, a postViewModel
is generated. This view model contains all the information of the post properly formatted and ready to be displayed in the UI. The view model then is passed to the template renderer which will simply generate the final HTML and display it to the user.
The template renderer, the router, and the server are also part of the web
package but their implementation is omitted from this post. You can check their code on GitHub.
Having all these layers and components implemented, what is missing is just to put all the pieces together. The goal of the app
layer is to provide this wiring. This component knows every other component and understands which dependency goes to each component. Here is 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) }
The app.Context
module acts like a Dependency Injection container. When asked for the webserver, for example, it knows how to build all the dependencies from every layer for it to work. The same applies to any other component in the application.
The final part is the main file. The implementation is rather simple as it is just an entry point for the app:
// cmd/web/main.go package main import ( "log" "github.com/geisonbiazus/blog/internal/app" ) func main() { c := app.NewContext() log.Fatal(c.WebServer().Start()) }
The main
function just creates an app.Context
, gets the web server, and starts it.
Uncle Bob says that good architecture maximizes the decisions not made. And that's exactly what this is. By having every low-level detail of your application implemented as an adapter, you don't have the pressure of doing the "right" decision from the start, because, in reality, the right decision is always attached to the time of the implementation.
Something that works now, will probably not be the best option in some months or years. Sometimes we take some shortcuts and add some technical debts on purpose to meet deadlines. That's why having a good architecture to keep this under control is a requirement.
The concepts of Clean Architecture make the development and maintainability of the software a lot easier. You don't need to spend much time deciding your infrastructure early on. If anything needs to be changed, as long as the business rules remain the same, it is just a matter of implementing a new adapter. If you need to change a business rule, you do it with minimal changes on the adapters, sometimes no change at all.
This is the greatness of this approach. Things are easy to change, easy to test, and everything has its place on the codebase.