How I organize (most of) my Go microservices

How I organize (most of) my Go microservices

November 6, 2022

An image of a blue Gopher in front of a board planning code organization

When first learning Go, my initial instinct was to find a framework that would scaffold the basic folder structure and functionality that would allow me to build my first project. It didn’t take long to realize that Go, thanks to its rich standard library ecosystem, didn’t really need a framework, and often the framework works against you. Since then, I’ve welcomed many new developers to my team who were used to working with different programming languages. Some of them have been surprised by the lack of a consistent folder structure across different parts of our company’s codebase.

One big advantage of not using a pre-packaged boilerplate project is that you and your team have the freedom to structure the project in a way that works best for your unique needs. Of course, there are some patterns that pop up again and again, and in those cases, I’ve found it helpful to create template projects that we can use as a starting point. But the key is to keep those templates simple and flexible, so we can swap out components easily if we need to.

First: a few ground rules

When creating Go packages, I try to follow these rules:

  • keep the package name as simple as possible, if possible in a single word.
  • keep it all lowercase, with no separation.
  • keep the package name singular.
  • For each package, I usually have a .go file with the same name, declaring the main interface or struct of the package.
  • For non-package folders, it is ok to have a multi-word name, but separate them by -, not _.

The minimal folder structure

.
├── 📁 .local
├── 📁 api
├── 📁 build
├── 📁 deployment
├── 📁 cmd
├── 📁 docs
│   ├── 📁 architecture
│   │   └── 📁 decision-records
│   ├── 📁 develop
│   ├── 📁 release-notes
│   └── 📁 runbooks
├── 📁 scripts
└── 📁 internal
    ├── 📁 app
    ├── 📁 logger
    ├── 📁 metrics
    └── 📁 server

📁 .local

The .local folder is where I stash all the stuff I need to run the app on my own machine, but that doesn’t need to be included in our deployment process. This includes things like secrets, docker-compose files, debugging libraries, and so on. I should note that not everything in this folder gets committed to git.

📁 api

The api folder is where we store all the files that define our API for different protocols. For example, we might have OpenAPI specs for REST, .proto files for gRPC, GraphQL schemas, and so on. Basically, anything that helps us communicate with other systems using different protocols goes in here.

📁 build

The build folder contains all the scripts and components needed to create images and binaries for both deployment and local development. This includes things like Dockerfile, package configuration scripts, and other build-related files. By having everything related to building and packaging in one place, it’s easier to manage and maintain our build process.

deployment

The deployment folder contains all the deployment-specific files, such as helm charts and templates.

📁 docs

These are (mostly) markdown files. Some of them are rendered by tools like Backstage or Mkdocs, and some are targeted toward developers working on the project and can be rendered in their IDE of choice.

Usually contains the following subfolders:

  • architecture: This is where I keep diagrams, decision records, etc
  • develop: This is the developer’s target documentation. E.g. how to debug a specific system, how to install a given library locally, and so on…
  • release-notes: here we keep a semantic version file with the release notes, explaining what has been fixed and which new features have been deployed.
  • runbooks: runbooks are a set of instructions on how to debug/solve production problems if we’ve seen them before.

📁 scripts

This is where I store all of the scripts, mostly written in Bash/Shell, but sometimes in Go as well. These scripts are used for various tasks such as updating libraries, installing dependencies, and so on.

These scripts are invoked a make command. By keeping them here, we can keep the Makefile (at the root of the project) simple and clean.

📁 cmd

n the cmd folder, you’ll find the entry points for the application. Each binary is represented by a folder with the same name, and inside it, you’ll typically find a small main.go file. This setup allows the main.go files to stay concise since the heavy lifting is done by the packages inside the internal (or sometimes pkg) directory. The app package configures and instantiates these packages. More details on that below.

📁 internal

The internal directory contains all the private packages and libraries that comprise the service. The code here is not meant to be imported by other applications.

In most of my projects, four internal packages are very often found: app, server, logger, and metrics. The options pattern is often present in these packages. It makes them flexible and re-usable since the variable configuration is coming from without, for example:

└── 📁 package
    ├── 📄 package.go
    └── 📄 option.go

The app package is the only one instantiated and ran directly from the cmd folder. I like to have it with the following signature

type App interface {
  New(context.Context, ...Options)
  Run(context.Context)
}

The app package is responsible for configuring (through respective options) all the other packages and injecting their dependencies. This pattern enables a simple use of Dependency Injection and configurability for each package.

It will also deal with gracefully shutting down all the other components if we receive a signal.

The other internal packages you will typically find in my boilerplate repository are:

  • 📁 logger: usually a wrapper around popular logging libraries, to adapt to my own use case.
  • 📁 metric: all the metrics, observability, and code-level monitoring. Likely prometheus logic, etc.
  • 📁 server: this is the actual server running the application. Because some projects I use REST, some gRPC, some both (and on rare occasions, GraphQL), I like to have my interface as:
 type Server interface {
   Start(context.Context, chan error)
   Stop(context.Context)
 }

Then each file http.go, grpc.go, graphql.go will implement the protocol-specific logic.

📄 Root files

In the root of the project, you will also typically find the following files (amongst others):

  • 📄 .gitattributes
  • 📄 .gitignore
  • 📄 .golangci.yaml
  • 📄 .editorconfig
  • 📄 CODEOWNERS
  • 📄 CONTRIBUTING.md
  • 📄 LICENSE.md
  • 📄 Makefile
  • 📄 README.md
  • 📄 SECURITY.md
  • 📄 go.mod
  • 📄 go.sum

These also include other files like sonar-project.properties if using Sonarqube, catalog-info.yaml if using Backstage, and so on.

Final project skeleton

.
├── 📄 .gitattributes
├── 📄 .gitignore
├── 📄 .golangci.yaml
├── 📄 go.mod
├── 📄 go.sum
├── 📄 .editorconfig
├── 📁 .local
│   ├── 📄 docker-compose.yaml
│   └── 📄 docker-compose.debug.yaml
├── 📄 CODEOWNERS
├── 📄 CONTRIBUTING.md
├── 📄 LICENSE.md
├── 📄 Makefile
├── 📄 README.md
├── 📄 SECURITY.md
├── 📁 api
│   └── 📄 openapi.json
├── 📁 build
│   └── 📄 Dockerfile
├── 📁 deployment
│   └── 📁 helm
│       ├── 📁 templates
│       ├── 📄 .helmignore
│       └── 📄 Chart.yaml
├── 📁 cmd
│   └── 📁 myapp
│       └── 📄 main.go
├── 📁 docs
│   ├── 📁 architecture
│   │   └── 📁 decision-records
│   ├── 📁 develop
│   ├── 📁 release-notes
│   │   ├── 📄 template.md
│   │   └── 📄 v1.0.0.md
│   └── 📁 runbooks
├── 📁 scripts
└──📁 internal
    ├── 📁 app
    │   ├── 📄 app.go
    │   └── 📄 option.go
    ├── 📁 logger
    │   ├── 📄 logger.go
    │   └── 📄 option.go
    ├── 📁 metrics
    │   ├── 📄 prometheus.go
    │   └── 📄 option.go
    └── 📁 server
        ├── 📄 grpc.go
        ├── 📄 http.go
        ├── 📄 option.go
        └── 📄 server.go

Next Steps: extracting reusable packages as dependencies

As you can see, some folders inside the internal directory may end up being very similar across multiple microservices. In some cases, these can be extracted as separate modules and imported as dependencies. This approach is particularly useful if you want to update the package in one place and allow individual services to control their dependency versions by choosing when to upgrade.

The logger and metrics packages are great examples of packages that can be extracted as dependencies, as they are not likely to change significantly from project to project.