Building a monolith in Go - Layout
Tags: go, programming
Published: 2024-05-01 12:45:43

_I'm building a reasonably sized commercial app in Go. By sharing decisions and choices (using a fictional restaurant app) I am hoping it may solicit advice from better developers, possibly help others, and generally force me to reflect when writing it out._

The crucial point in scaling a business is when you hire enough developers to be able to specialise effort. That jump from one, two, or five developers working on a shared code-base to multiple teams with their own responsibilities.

This is the moment you have to completely refactor the spaghetti, or have an easy ride to a service architecture. Or probably somewhere in between. Having done this many times I think it's possible to have an easy ride, if and when the time comes to scaling.

Go's package system is very simple, and really suits separated concerns in distinct repo's. However, the practicality of continuously updating local copies becomes a real drag for a small startup. Deploying and versioning multiple services adds to the friction and cost.

By contrast, a monolith is simple. Particularly with Go packages, a monolith layout can emulate a service architecture. If this is possible then we gain simple deployments and a simple dev process.

The key is to build and layout the monolith in a way that makes it easy to extract components later on. While avoiding the (process, infrastructure) overhead of a micro-service architecture from day one.

### Package Layout

My fictional restaurant Go Eat has five principle areas of concern. So I create a top-level package for each:

```
./goeat/booking
./goeat/menu
./goeat/staff
./goeat/service
./goeat/kitchen
```

I have tried so many layouts that I now realise layout is not strong enough on its own to force good habits. It would be very easy to create a mess from this layout, with deep calls between packages that are hard to unpick.

### Domain Boundaries

One of the nasties that hits companies when the try and scale is _monolith mess_. Code execution paths that are so intertwined that the pragmatic path to scale is rewriting the app. That's a costly way of scaling, but at least you know what it _should_ do.

I want to avoid creating this mess by using Go's `internal` package qualifier. For example, when writing functions in `kitchen` I want to prevent dipping into `staff` because it's convenient.

Go's package export method (capitalised type names) is too lax for my case, at the service/domain/parent-package level. I want to protect package exports inter-service, but still use them intra-service.

To solve this I am using a clearly named `publicapi.go` file at the top of each (service) package tree. And by protecting all other functions under `internal` I have enforcement.

```
./goeat/kitchen/
./goeat/kitchen/internal/rota.go

./goeat/staff/
./goeat/staff/publicapi.go
./goeat/staff/internal/calendar/main.go
```

My `kitchen` service needs to update its `rota` from the `calendar` package in the `staff` service. So I create a public API route into `staff` like this:

```go
// ./goeat/staff/publicapi.go

package staff

import "goeat/staff/internal/calendar"

var (
        GetKitchenRota = func() []string {
                return calendar.GetRota("kitchen")
        }
)
```

It's really just a local version of an http API. Which will make a future transition easier to do.

And calling this from the `kitchen` service looks like this:

``` go
// ./goeat/kitchen/internal/rota.go

package rota

import (
	"goeat/staff"
)

func fetchRota() []string {

	rota := staff.GetKitchenRota()

	// do something with the rota

	return rota
}
```

A readability bonus is the name of the import being `staff`. The domain is much clearer than using a sub-package export, which would result in `calendar.GetRota`.

### Testing

Implementing the public 'api' methods as vars enables me to isolate testing between services.

It is trivial to mock the responses from the `staff` service, independently of that package. I haven't found a simpler way of doing this, every other way seems to involve code gymnastics with interfaces or channels. This particular use-case is emulating services within a monolith.

A simple test example in the `kitchen` service:

```go
//go:build test

// ./goeat/kitchen/internal/rota/main_test.go

package rota

import (
	"goeat/staff"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestFetchRota(t *testing.T) {

	mockRota := []string{"A", "B"}
	mockStaffGetKitchenRota := func() []string {
		return mockRota
	}

	// override the 'api' response with our mock function
	staff.GetKitchenRota = mockStaffGetKitchenRota

	assert.Equal(t, fetchRota(), mockRota, "expect A, B")
}
```

In this way I can develop features in one service (`kitchen`) without touching the code in other services. Fewer typos, fewer bugs.

### Summary

My choice to develop an app as a monolith depends on a bunch of particular use-case factors.

I think Go's inherent simplicity around packages make it easy to create domain boundaries. This makes the whole code-base simpler to think about.

Simplicity is key, because with few (or solo) developers complexity kills motivation. Speed of development is often less about CPU cycles, and more about efficient process.

A single deployable binary is easier to manage and test, at small scale. Although Go suits a discrete service-architecture, the overhead (repo's, ci, infra) is too much right now.

The sample code is on GitHub, which I'll update as I go: [Go Eat](https://github.com/thisdougb/go-eat)