Monorepo with Go: Organizing Code in Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Monorepo with Go: Organizing Code in Practice

\n\n

A monorepo is an approach to source code management where all projects, libraries, and services are stored in a single repository. In the Go world, this approach has become especially popular due to built-in module support and a flexible package system. In this article, we will break down how to organize a monorepo in Go in practice, examine a typical structure, code examples, and best practices.

\n\n

Why Do You Need a Monorepo in Go?

\n\n

A monorepo solves several key problems:

\n
    \n
  • Simplifying dependency management — all internal packages are in one place, making them easy to update and refactor.
  • \n
  • Unified change history — atomic commits that affect multiple services at once work without extra synchronization.
  • \n
  • Faster CI/CD — you can set up pipelines that build only the changed parts of the project.
  • \n
  • Improved code quality — it's easier to enforce uniform standards, linters, and code reviews.
  • \n
\n\n

Go Monorepo Structure: Basic Layout

\n\n

Let's look at a typical Go monorepo structure. For example, let's take a project consisting of two microservices (api and worker) and a shared library lib:

\n\n
my-monorepo/\n├── go.work\n├── go.work.sum\n├── apps/\n│   ├── api/\n│   │   ├── go.mod\n│   │   ├── main.go\n│   │   └── handler.go\n│   └── worker/\n│       ├── go.mod\n│       ├── main.go\n│       └── process.go\n├── lib/\n│   ├── go.mod\n│   ├── auth/\n│   │   └── auth.go\n│   └── db/\n│       └── db.go\n├── scripts/\n│   ├── build.sh\n│   └── test.sh\n└── Makefile
\n\n

Key elements:

\n
    \n
  • go.work — the Go Workspace file that links all modules together.
  • \n
  • apps/ — the directory for applications (microservices, CLI utilities, etc.).
  • \n
  • lib/ — shared libraries used by all applications.
  • \n
  • scripts/ — auxiliary scripts for building, testing, and deployment.
  • \n
\n\n

Setting Up Go Workspace (go.work)

\n\n

Go 1.18+ provides the workspace mechanism, which allows you to work with multiple modules simultaneously. Let's create a go.work file in the root:

\n\n
go 1.22\n\nuse (\n    ./apps/api\n    ./apps/worker\n    ./lib\n)
\n\n

Now, during development, you can run commands from the project root, and Go will see all modules as a single unit. For example, to run tests for all modules:

\n\n
go test ./...
\n\n

And to build a specific service:

\n\n
cd apps/api && go build -o bin/api .
\n\n

Code Example: Shared Library and Microservice

\n\n

Let's create a simple authentication library in lib/auth/auth.go:

\n\n
package auth\n\nimport "errors"\n\nvar (\n    ErrInvalidToken = errors.New("invalid token")\n)\n\n// ValidateToken checks the token and returns the user ID.\nfunc ValidateToken(token string) (string, error) {\n    if token == "" {\n        return "", ErrInvalidToken\n    }\n    // Here could be real JWT validation or a call to an external service\n    return "user-123", nil\n}
\n\n

Now let's use this library in the microservice apps/api/handler.go:

\n\n
package main\n\nimport (\n    "encoding/json"\n    "net/http"\n    "my-monorepo/lib/auth"\n)\n\ntype UserResponse struct {\n    UserID string `json:"user_id"`\n}\n\nfunc UserHandler(w http.ResponseWriter, r *http.Request) {\n    token := r.Header.Get("Authorization")\n    userID, err := auth.ValidateToken(token)\n    if err != nil {\n        http.Error(w, "Unauthorized", http.StatusUnauthorized)\n        return\n    }\n\n    resp := UserResponse{UserID: user                

Blogs

Book Recommendations