What is a monorepository and why is it needed in Go?
A monorepository (monorepo) is a code management approach where all projects, libraries, and services are stored in a single repository. In the Go ecosystem, this approach is becoming particularly popular due to built-in support for modules and workspaces.
For beginner developers, a monorepository offers several key advantages:
- Single code version — all dependencies are synchronized
- Simplified refactoring — changes are immediately visible across all projects
- Standardization — consistent linter, CI/CD, and test settings
- Transparency — easy to understand how different parts of the system are connected
In this article, we will break down how to properly organize a monorepository in Go, starting from the basic structure and ending with practical examples.
Basic structure of a Go monorepository
Proper code organization is the foundation of a successful monorepository. Let's look at a typical structure:
my-monorepo/├── apps/│ ├── web-api/ # Main application│ │ ├── main.go│ │ ├── go.mod│ │ └── internal/│ └── worker/ # Background worker│ ├── main.go│ └── go.mod├── libs/│ ├── config/ # Shared configuration│ │ ├── config.go│ │ └── go.mod│ └── logger/ # Logging│ ├── logger.go│ └── go.mod├── scripts/ # Helper scripts├── docs/ # Documentation├── go.work # Go workspace└── README.mdKey principles of this structure:
- apps/ — executable applications (binaries)
- libs/ — reusable libraries
- scripts/ — automation and utilities
- docs/ — project documentation
Setting up Go Workspace for a monorepository
Starting with Go 1.18, official support for workspaces was introduced via the go.work file. This allows working with multiple modules simultaneously without needing to publish them to a remote repository.
Let's create a workspace for our monorepository:
// go.workgo 1.22
use ( ./apps/web-api ./apps/worker ./libs/config ./libs/logger)After creating go.work, all modules in the use list will be accessible to each other locally. This is especially convenient during development — changes in a library are immediately visible in the applications.
Initializing modules
Each component needs its own go.mod. Example for the configuration library:
// libs/config/go.modmodule github.com/your-org/libs/config
go 1.22And for the web-api application:
// apps/web-api/go.modmodule github.com/your-org/apps/web-api
go 1.22
require github.com/your-org/libs/config v0.0.0
replace github.com/your-org/libs/config => ../../libs/configNote the replace directive — it tells Go to use the local version of the library instead of searching for it online.
Code example: library and application
Let's create a simple configuration library and an application that uses it.
Library libs/config
// libs/config/config.gopackage config
import ( "fmt" "os")
type Config struct { Port string Env string}
func Load() (*Config, error) { port := os.Getenv("PORT") if port == "" { port = "8080" }
env := os.Getenv("ENV") if env == "" { env = "development" }
return &Config{ Port: port, Env: env, }, nil}
func (c *Config) String() string { return fmt.Sprintf("Config{