Updated Nov 5, 20266 min read

Go Project Structure: How to Lay Out a Go Backend in 2026

What the Go team actually recommends for packages, cmd and internal, where the popular project-layout repo fits, and how to structure a Go backend once it has a database, queues and more than one service.

Go projects start simple: a go.mod, a main.go and maybe a second file. The structure questions show up later, when a handler package imports a database package that imports a config package that needs the handler's types, or when a second binary appears and nobody is sure whether shared code belongs in pkg/, internal/ or common/.

This guide covers what the Go team recommends, what the popular community layout adds, and how to lay out a Go backend that has grown into several services, starting with the layout Encore (a tool that works alongside your framework) uses, where the directory structure also defines your services and their infrastructure.

Go project layouts at a glance

Encore service layoutFlat packagecmd + internal (go.dev)golang-standards/project-layout
ShapeOne package per service, infra declared insideEverything in one packageBinaries in cmd/, logic in internal/Many top-level directories (cmd, internal, pkg, api, configs, ...)
OfficialEncore convention on standard GoYesthe go.dev starting pointYesgo.dev for servers and multiple binariesNoa community collection
Service boundariesExplicit, enforced by the compiler and EncoreNoneBy conventionBy convention
Best forBackends with databases, queues and several servicesSmall tools and early prototypesMost Go servers and CLIsTeams that want a familiar checklist

Each option is covered below, starting with Encore.

Packages as services with Encore

Encore is a tool that works alongside your framework for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. In a Go app, a package that defines at least one //encore:api endpoint is a service, and the infrastructure it owns, such as a Postgres database or a Pub/Sub topic, is declared in that package. The app structure guide recommends starting with one service and adding more only when a capability has its own data and failure behavior.

Application code passes through Encore and infrastructure guardrails to resources provisioned in your own AWS or GCP account.
Encore provisions the infrastructure your application needs in your own AWS or GCP account.

A shop backend with orders and notifications looks like this on disk:

~/shop
shop/├── encore.app├── go.mod // module encore.app├── orders/ // orders service│ ├── orders.go // API endpoints│ ├── events.go // Pub/Sub topic owned by orders│ ├── store.go // queries against the orders database│ └── migrations/│ └── 1_create_orders.up.sql└── notifications/ // notifications service └── subscriptions.go // reacts to order events
Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

The orders package declares its database and topic next to the endpoint that uses them:

// orders/orders.go package orders import ( "context" "encore.dev/pubsub" "encore.dev/storage/sqldb" ) var db = sqldb.NewDatabase("orders", sqldb.DatabaseConfig{ Migrations: "./migrations", }) type OrderPlaced struct { OrderID int64 `json:"order_id"` Email string `json:"email"` } var OrderPlacedTopic = pubsub.NewTopic[*OrderPlaced]("order-placed", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) type PlaceParams struct { Email string `json:"email"` SKU string `json:"sku"` } type PlaceResponse struct { OrderID int64 `json:"order_id"` } //encore:api public method=POST path=/orders func Place(ctx context.Context, p *PlaceParams) (*PlaceResponse, error) { var id int64 err := db.QueryRow(ctx, `INSERT INTO orders (email, sku) VALUES ($1, $2) RETURNING id`, p.Email, p.SKU).Scan(&id) if err != nil { return nil, err } if _, err := OrderPlacedTopic.Publish(ctx, &OrderPlaced{OrderID: id, Email: p.Email}); err != nil { return nil, err } return &PlaceResponse{OrderID: id}, nil }

The migration in orders/migrations creates the orders table the queries use:

-- orders/migrations/1_create_orders.up.sql CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL, sku TEXT NOT NULL );

The notifications service imports the topic like any other Go identifier:

// notifications/subscriptions.go package notifications import ( "context" "encore.app/orders" "encore.dev/pubsub" ) var _ = pubsub.NewSubscription(orders.OrderPlacedTopic, "send-confirmation", pubsub.SubscriptionConfig[*orders.OrderPlaced]{ Handler: sendConfirmation, }) // sendConfirmation may run more than once for the same order (at-least-once // delivery), so the email provider call should use OrderID as an idempotency key. func sendConfirmation(ctx context.Context, e *orders.OrderPlaced) error { return nil }

A synchronous call to another service is also a plain function call, for example billing.Charge(ctx, req) after importing encore.app/billing, and Encore turns it into a typed network call with tracing. There is no main.go, since Encore generates the entry point, and no separate wiring for connection strings: encore run starts Postgres locally, applies the migrations and runs both services, with the Pub/Sub topic delivering messages between them.

The structure is also what Encore deploys: each service package can run as its own process on Fargate or Cloud Run, the topic becomes SNS and SQS on AWS or Pub/Sub on GCP, and the database becomes RDS or Cloud SQL in your own account. An agent reading the repository sees which service owns which table and topic by looking at one directory, which keeps its changes inside the right boundary.

Helper packages work as usual: a service can have subpackages such as orders/internal/pricing, as long as only the service package itself defines endpoints.

Start flat

The official Organizing a Go module guide starts with a single package at the module root, and that is the right layout for a small tool or the first weeks of a service. Files split by topic (handlers.go, store.go) inside one package can call each other freely, and there are no import cycles to manage.

Move on when the package is hard to navigate or when you want to hide implementation details. Splitting into packages too early mostly creates cycles and exported names that exist only to cross a package line.

cmd and internal: the go.dev server layout

For servers and multi-binary projects, the go.dev guide recommends putting the server's logic in an internal directory and each binary in cmd/<name>/main.go. Packages under internal/ can only be imported by code rooted at the parent of internal, which the Go command enforces, so you can refactor them without breaking anyone outside the module.

Inside internal, group by feature (internal/orders, internal/billing) rather than by layer (internal/handlers, internal/models, internal/repository). Layer packages tend to import each other in both directions as features grow, while feature packages keep a change to orders inside the orders directory.

golang-standards/project-layout

The project-layout repository is the layout most search results show, with cmd, internal, pkg, api, configs, deployments and more. Its own README says it is not an official standard defined by the core Go team, and points to the go.dev page for the official guidance.

It is useful as a checklist of where things commonly go, but adopting every directory up front gives a small service a dozen mostly empty folders. The pkg/ directory in particular is optional: code that is not meant for other modules belongs in internal/.

Good to know

Package structure in a plain Go server says nothing about deployment. Whether internal/orders and internal/billing share a database, run in one process or talk over a queue is decided elsewhere, in Terraform, Compose files and environment variables, and the layout and the infrastructure drift apart over time. Keep a short README per top-level package that names the data it owns.

How to choose

Start with a flat package and move to cmd/ plus internal/ with packages grouped by feature once you have a server or more than one binary, and treat golang-standards/project-layout as a reference rather than a template.

Use the Encore layout when the Go backend has a database, queues and more than one service, and you want each package to declare the infrastructure it owns, run locally with encore run and deploy to your own AWS or GCP account from the same structure.

Frequently asked questions

What is the recommended Go project structure?

Start flat: a go.mod and a few files in one package. When the project grows, the official Organizing a Go module guide on go.dev recommends putting server logic in an internal directory, which other modules cannot import, and putting each binary under cmd/<name>. Group packages by what they do, such as orders or billing, rather than by layer.

Is golang-standards/project-layout the official Go layout?

It is not, and its README states that it is not an official standard defined by the core Go team but a collection of common patterns, and directories such as pkg/ are optional. The official guidance is the Organizing a Go module page on go.dev.

How do I structure a Go backend with several services?

Keep one module and give each service its own package with its own data. With Encore, any package that defines an //encore:api endpoint is a service, other services call it as a normal Go function such as billing.Charge(ctx, req), and asynchronous work goes through a pubsub.NewTopic declared in the publishing package.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders