Ever since I started writing web applications, or more specifically REST APIs in Go, I’ve been looking for the perfect way to structure my code and files containing said code. I have “moved” to Go from PHP, where I was pretty comfortable with a sort of an MVC architecture, but when I made the move to Go I was lost. I’ve built a couple of simple applications before in Go, with little issue, but when I started designing the backend systems for Viberate.com I just couldn’t get comfortable. This was my first service based backend system written in Go. I had the base layout of the whole system thought out, but when it came to writing the code, where should I put what, how should I use it, what are the benefits and detriments of one approach versus the other were just some of my questions.

I’ve decided pretty early on that I want 3 basic types of layers throughout each service:

  • Repository
  • Service
  • Handler

The Repository would handle persistence of the data to and from the database, the Service would contain all business logic, and the Handler would handle the communication between the web interface and the Service. But the next question arose, where to put them? Should I join them together in logical entities, and create a new package for each logical entity, like the Artist, Venue, or the user? Or should I stick each layer in their own package, regardless to which logical entity they belong to? I have probably went back and forth on those questions a dozen times. When I was finally happy with one way I would try to go forth with writing the code, but would quickly find something missing, or be impossible without some “hacky” workaround. Now, I can’t exactly tell you which way I chose in the end, without breaking my NDA, but let it suffice when I say, that I was not completely happy with it, but I accepted the flaws and drawbacks of my choice, and went on with developing the system to satisfy the requirement of the system being built in the first place.

Onion/Layered Architecture And Domain Driven Design

Even as I was joined by other team members, we continued developing our software on the “architecture” which I laid out, with minor alterations made to it throughout the time of development, I still wasn’t satisfied with it. I’ve read and re-read extensively on the subject on the web, coming across the onion and layered architectures, and with it, of course Domain-Driven-Design.

Since DDD is really a whole lot than just the architecture of the code, I will just skim it in this article, but I invite you to learn about it. This article will focus more on the Onion/Layered code architecture. While we can find some DDD concepts in the architectures domain layer it is important to point out that they are not the same thing, and we can use Onion without using DDD.

Onion

Imagine an onion, or if your imagination is not that good, you know, look at the picture above. An onion is comprised of different layers of its flesh, and just like the vegetable, your code in the Onion architecture is layered in the same way, like an onion. In the outermost layer, we have the Infrastructure layer, which holds code that interacts with the database i.e., then one layer lower, we find the Interface layer, which holds the interface code, like the HTTP handler. Peeling away one more layer we come to the Application layer, which is more of a passage, passing the request from the interface layer, to the next and final layer, the Domain layer, which holds all of the applications business logic.

It is important to note, that all communication between the layers is done only in one way, inward. At the center, the domain layer, does not know, nor does it need to know about any above layers. It provides logic that enables it to do its job, and interfaces for its dependencies, like the repositories.

I have also created a small example API with the Onion architecture, which can be found on gitlab. I advise you to visit it, as this example is used in this post from now on, for a small walkthrough. The example API is also available publicly on https://beer.tyr.lovrec.dev/api/v1/. Note that the database is rolled back every full hour.

Before we dive into the example, I must point out that this is my personal interpretation of the architecture, and is not necessarily 100% correct.

Now, let’s dive into the example.

Domain layer

Lets begin with the domain layer. We have the Beer entity struct defined in the domain/beer.go file, defining its properties and methods. Towards the end the BeerRepo interface is defined, which holds the Create and Update function signatures which the Beer entity uses to persist itself in the Save method. The entity itself doesn’t care how it is persisted, be it in a relational database, or a flat file on the file system, as long as the repository implements the BeerRepo interface.

The InitBeerRepo function is used later in the container to inject the repository that implements the BeerRepo interface.

package domain

import "context"

type Beer struct {
	tableName struct{} `pg:"beer"` // used by go-pg

	ID             int64        `json:"id" pg:",pk,notnull"`
	Name           string       `json:"name" pg:",notnull,unique" validate:"required"`
	Alcohol        float64      `json:"alcohol" pg:",user_zero"`
	ManufacturerID int64        `json:"manufacturer_id,omitempty" pg:",notnull" validate:"required"`
	Manufacturer   Manufacturer `json:"manufacturer,omitempty" pg:",notnull" validate:"structonly"`

	Model // define created/updated at props and embed
}

func (b *Beer) Save(ctx context.Context) error {
	if b.ID == 0 {
		return beerRepoImpl.Create(ctx, b)
	}

	return beerRepoImpl.Update(ctx, b)
}

type BeerRepo interface {
	Create(context.Context, *Beer) error
	Update(context.Context, *Beer) error
}

var beerRepoImpl BeerRepo

func InitBeerRepo(impl BeerRepo) {
	beerRepoImpl = impl
}

func GetBeerRepo() BeerRepo {
	return beerRepoImpl
}

Infrastructure layer

In the infrastructure layer, we have the repository implementation that satisfies the BeerRepo interface of the domain layer in infrastructure/database/beer.go. In the Beer API example we interact only with the database, using the go-pg library:

package database

import (
    // ...
	pgwrapper "gitlab.com/slax0rr/go-pg-wrapper"
)

type Beer struct {
	db pgwrapper.DB
}

func NewBeer(db pgwrapper.DB) *Beer {
	return &Beer{db}
}

func (p *Beer) Create(ctx context.Context, m *domain.Beer) error {
	_, err := p.db.WithContext(ctx).Model(m).Returning("id").Insert()
	if err != nil {
		return err
	}

	return nil
}

func (p *Beer) Update(ctx context.Context, m *domain.Beer) error {
	res, err := p.db.WithContext(ctx).Model(m).
		Column("name", "alcohol", "manufacturer_id", "updated_at").
		WherePK().
		Update()
	if err != nil {
		return err
	}

	if res.RowsAffected() == 0 {
		persErr := ErrRowNotFound
		persErr.Details = struct{ ID int64 }{m.ID}
		return persErr
	}

	return nil
}

The Create and Update methods do just that what their names suggest, create and update beer records in the database. If the Update method does produce any affected rows it is assumed that the beer that the method was about to update does not exist.

Interface layer

The interface layer holds the HTTP handler in infrastructure/http/handlers/beer.go, which handles incoming requests on the beer resource by binding them to the structs or entities of the domain layer, passing them on to the application layer, waiting for a response, and writing said response back to http:

package handlers

import (
	"net/http"

    // ...
	httphelper "gitlab.com/slax0rr/go-beer-api/interface/http"
)

type Beer struct {
	beerApp application.Beer
	http    httphelper.HTTPHelper
}

func NewBeerHandler(
	beerApp application.Beer,
	http httphelper.HTTPHelper,
) *Beer {
	return &Beer{beerApp, http}
}

func (h *Beer) Register(rtr *mux.Router) {
	r := rtr.PathPrefix("/beer").Subrouter()

	r.Path("").HandlerFunc(h.Create).Methods(http.MethodPost)
	r.Path("/{id:[0-9]+}").HandlerFunc(h.Update).Methods(http.MethodPut)
}

func (h *Beer) Create(w http.ResponseWriter, req *http.Request) {
	beer := new(domain.Beer)
	if err := h.http.ParseRequest(w, req, beer); err != nil {
		return
	}

	err := h.beerApp.Create(req.Context(), beer)
	if err != nil {
		logrus.WithError(err).Error("unable to create a new beer")
		h.http.SendError(w, httphelper.ErrInternalServerError)
		return
	}

	h.http.SendResponse(w, beer, http.StatusCreated)
}

func (h *Beer) Update(w http.ResponseWriter, req *http.Request) {
	beer := new(domain.Beer)
	if err := h.http.ParseRequest(w, req, beer); err != nil {
		return
	}

	// an error can not occur, since the regex in the path already limits to positive numbers
	beer.ID, _ = strconv.ParseInt(mux.Vars(req)["id"], 10, 64)

	err := h.beerApp.Update(req.Context(), beer)
	switch {
	case err == nil:
		h.http.SendResponse(w, beer, http.StatusOK)

	case err.Error() == database.ErrRowNotFound.Error():
		logrus.WithField("error", err).Info("requested beer not found")
		h.http.SendError(w, httphelper.ErrNotFound)
		return

	default:
		logrus.WithError(err).
			WithField("id", beer.ID).
			Error("unable to update the beer")
		h.http.SendError(w, httphelper.ErrInternalServerError)
		return
	}
}

The above handler definition provides the Register method, making it in effect a handler that can be registered in the app.go handler registration loop, and the Create and Update methods which handle the POST /beer and PUT /beer/{id} API calls.

The Create method parses the request by binding the incoming JSON to the domain.Beer struct, passing on the newly created object to the application layer, and writing the successfully created beer object back to the interface or writing an error response to the interface in case of an error.

The httpJSONHelperImpl is used to decode and encode the request and response which can be quickly changed out for a different helper implementation if it would be needed. As a next step here, the correct http helper implementation should be chosen, based on the requests Content-Type header, but for the sake of the example this approach is fair enough.

Application layer

The application layer acts as link between the interface and the domain layer, handling any application specific logic. The example defines a simple application layer struct in application/beer.go which receives the bound beer entity object, that is forwarded to the domain layer by calling its own Save method.:

package application

import (
    // ...
)

type Beer interface {
	Create(context.Context, *domain.Beer) error
	Update(context.Context, *domain.Beer) error
}

type BeerImpl struct{}

var _ Beer = new(BeerImpl)

func (a *BeerImpl) Create(ctx context.Context, beer *domain.Beer) error {
	if beer == nil {
		return fmt.Errorf("beer entity may not be nil")
	}

	return beer.Save(ctx)
}

func (a *BeerImpl) Update(ctx context.Context, beer *domain.Beer) error {
	if beer == nil {
		return fmt.Errorf("beer entity may not be nil")
	}

	return beer.Save(ctx)
}

Tying it all together

To help with initialising everything the infrastructure layer also provides a sort of a container, which is basically a small collection of factories, repository, and handler definitions which can be found in the infrastructure/container directory, split in three files:

  • data_access.go - holds DB initialisation function and repository listing
  • library.go - holds the factory for the HTTP response helper
  • handlers.go - provides a list of handlers and injects them with the HTTP response helper

The app.go file contains functions to start the web server, inject the repository definitions into the domain layer, load handlers and register them with the router. In all fairness, this part of the code belongs in the infrastructure layer, and will probably be refactored there in the future.

And at last, there is the cli directory, which holds the CLI commands to start the server or create the database schema. And again, in all fairness, this part belongs in the interface layer, and will also probably be refactored there in the future.

I invite you to clone the repository provided above and try it out. The Makefile provided should guide you through to get the API successfully compiled and started.

Conclusion

Using the Onion architecture, makes it much more easier and apparent where some piece of code belongs, and keeps those pieces de-coupled, which will be a nightmare as your software grows. The example on gitlab extends a little bit beyond this post, implementing a bit more logic than described here, and it provides some solutions to the problems I was facing in the past or I am still facing them.

I know this is far from perfect and I’ve come to realise, I will never find the perfect or one-size-fits-all solution to all of my problems, but at the moment, I think this is the closest I’ve come so far. Of course implementing simple solutions like the example API, on such an architecture is beyond overkill, but it is used as an example only.