How to Structure Multi-Binary Go Projects: The Standard Layout Explained

Place each binary in its own subdirectory under /cmd, share private implementation details in /internal, and expose public APIs in /pkg to keep entry points thin and maintainable.

Multi-binary Go projects—repositories that ship several executables such as APIs, background workers, and administrative CLI tools—require careful organization to avoid circular dependencies and tight coupling. The golang-standards/project-layout repository provides a battle-tested blueprint for structuring these projects, used by major Go codebases like Prometheus, Kubernetes, and Velero.

The Core Directories: /cmd, /internal, and /pkg

Three directories form the backbone of any multi-binary Go project. Each serves a distinct purpose in the dependency graph, ensuring that shared code remains accessible to your binaries while hidden from external consumers.

/cmd – One Directory Per Binary

The /cmd directory contains one subdirectory for every executable your project produces. According to the project's cmd/README.md, each folder must be named after the final binary it generates (e.g., cmd/api, cmd/worker, cmd/admin). Inside each folder, a single main.go file acts as a thin bootstrap layer that wires together reusable packages.

This isolation prevents entry points from accumulating business logic. When you run go build ./cmd/api, the compiler produces a binary named api that imports logic from elsewhere, keeping the main package clean.

/internal – Private Shared Implementation

The /internal directory houses packages that cannot be imported by external modules. As documented in internal/README.md, the Go compiler enforces this boundary—any package under an internal directory is accessible only to code within its parent module tree.

This is where you place configuration loaders, database adapters, and business logic shared across your binaries. For example, internal/app/api might contain HTTP handlers, while internal/app/worker contains queue consumers, both importing internal/pkg/config for shared initialization logic.

/pkg – Public Reusable Libraries

The /pkg directory contains libraries intended for external consumption. If a component of your project is useful to other Go modules—such as a generated gRPC client or a metrics wrapper—place it here. External projects can import these packages via go get, while your own binaries in /cmd treat them as first-party dependencies.

Real-World Directory Structure

A typical multi-binary project with an API server, background worker, and admin CLI tool follows this layout:


myproject/
├─ cmd/
│  ├─ api/
│  │  └─ main.go          # Entry point for the API server

│  ├─ worker/
│  │  └─ main.go          # Entry point for the background worker

│  └─ admin/
│     └─ main.go          # Entry point for the admin CLI

├─ internal/
│  ├─ app/
│  │  ├─ api/            # Business logic for the API binary

│  │  ├─ worker/         # Business logic for the worker binary

│  │  └─ admin/          # Business logic for the admin binary

│  └─ pkg/
│     └─ config/         # Private shared packages (e.g., config loader)

├─ pkg/
│  └─ client/            # Public library for external projects

├─ configs/
│  ├─ api.yaml
│  ├─ worker.yaml
│  └─ admin.yaml
└─ go.mod

Each cmd/*/main.go imports from internal/app/... for domain logic and pkg/... for public utilities. Ancillary assets like configuration templates live in /configs, with one file per binary to allow environment-specific tuning.

Why This Layout Works with Go Tooling

The Standard Go Project Layout aligns with the compiler's built-in protections and community conventions. The internal directories are enforced by the toolchain—attempting to import github.com/yourorg/myproject/internal/pkg/config from an external module results in a compilation error. This guarantees encapsulation without relying on documentation alone.

Furthermore, placing binaries under /cmd mirrors the structure used by the Go ecosystem's largest projects. The cmd/README.md in the golang-standards repository explicitly references Prometheus, Kubernetes, and Velero as projects following this convention. Because the layout uses a single go.mod at the root, all binaries share versioned dependencies while remaining importable via distinct subpaths.

Implementing the Pattern

The following examples demonstrate how to keep main.go files minimal while maximizing code reuse across binaries.

Entry Point in cmd/api/main.go

This entry point imports internal business logic and a public client library, delegating all work to imported packages:

package main

import (
    "log"

    "github.com/yourorg/myproject/internal/app/api"
    "github.com/yourorg/myproject/pkg/client"
)

func main() {
    cfg, err := api.LoadConfig()
    if err != nil {
        log.Fatalf("config error: %v", err)
    }

    c := client.New(cfg.APIKey)
    if err := api.StartServer(c, cfg); err != nil {
        log.Fatalf("server failed: %v", err)
    }
}

The api package (located in internal/app/api) handles server initialization, while client (from pkg/client) provides a public API for external HTTP calls.

Shared Configuration in internal/app/config/config.go

Place reusable initialization code in internal to share across binaries without exposing implementation details:

package config

import "github.com/spf13/viper"

type Config struct {
    APIKey string `mapstructure:"api_key"`
    Port   int    `mapstructure:"port"`
}

// LoadConfig reads a YAML file from ./configs/<binary>.yaml.
func LoadConfig(name string) (*Config, error) {
    v := viper.New()
    v.SetConfigName(name)
    v.AddConfigPath("./configs")
    v.SetConfigType("yaml")
    
    if err := v.ReadInConfig(); err != nil {
        return nil, err
    }

    var cfg Config
    if err := v.Unmarshal(&cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}

Both cmd/api/main.go and cmd/worker/main.go can call config.LoadConfig("api") or config.LoadConfig("worker"), ensuring consistent configuration parsing while keeping binary-specific settings isolated in /configs.

Public Client Library in pkg/client/client.go

Libraries intended for external consumption live in /pkg with exported functions:

package client

type Client struct {
    APIKey string
}

// New creates a client instance.
func New(apiKey string) *Client {
    return &Client{APIKey: apiKey}
}

// DoSomething is an exported function usable by any Go module.
func (c *Client) DoSomething() error {
    // implementation
    return nil
}

External projects import github.com/yourorg/myproject/pkg/client, while your internal binaries treat it as a standard dependency.

Summary

  • Use /cmd/<binary>/main.go for each executable to isolate entry points and keep bootstrap code minimal.
  • Store private shared code in /internal to leverage the compiler's import restrictions and protect implementation details.
  • Place public libraries in /pkg to expose stable APIs for external consumption.
  • Reference the golang-standards/project-layout repository for community-validated conventions used by Kubernetes, Prometheus, and Velero.

Frequently Asked Questions

What is the difference between /internal and /pkg?

The /internal directory contains packages that the Go compiler restricts to the current module—external projects cannot import them. This is ideal for business logic and database adapters. The /pkg directory contains public libraries intended for external consumption, allowing other Go modules to import and reuse your code via go get.

How do I handle configuration files for multiple binaries?

Create a /configs directory at the project root with one configuration file per binary (e.g., api.yaml, worker.yaml). Store a shared loader in internal/pkg/config that accepts the binary name as a parameter, allowing each cmd/*/main.go to load its specific settings while reusing parsing logic.

Can external projects import code from my multi-binary repository?

Yes, but only from /pkg and any other top-level directories outside of /internal or /cmd. The internal directory is protected by the Go compiler, ensuring that your implementation details remain private. Public APIs in /pkg should be versioned and documented for stability.

Where should I put business logic that is shared between binaries?

Place domain logic in /internal/app/<binary> for code specific to one executable, and in /internal/pkg or /internal/app/common for utilities shared across multiple binaries. This keeps shared code private to your module while allowing each binary to import exactly what it needs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →