Dev Skills

How to lay a Go project out

What a package is, the three layouts you will meet, and what each common folder is for.

Topic 54 of 98 · 10 min to read · 50 min to build
After this topic you can
  • Explain what a package is and how internal/ restricts imports.
  • Compare the flat, by-layer and by-domain layouts and pick one for a given project.
  • Name packages by what they provide, avoiding utils and common.

First, what a package is

A package is a folder of .go files that are compiled together and share a name. Every file in one folder must declare the same package name, and that name is how other code refers to it.

Packages are the only unit of privacy Go has. A name starting with a capital letter can be used from outside the package. A lowercase name cannot. There is no public, private or protected keyword. The folder boundary plus capitalisation is the whole system.

A module is one level up. It is a whole project: a folder with a go.mod file, containing one or more packages. The module path in go.mod is the prefix for every import inside it.

What "project layout" means

Laying out a project means deciding which folders exist and what goes in each one. In Go the folder structure decides three things: what can import what, what outsiders can use, and how much you have to read to understand one feature.

Go has no official required layout. What it has instead is one enforced rule (internal/, below) and a set of conventions that most teams follow. Below are the three layouts you will meet most, smallest first.

Layout 1 — flat

Everything in one package at the top of the repository. There are no subfolders.

Use it for: a command-line tool, a script, a small library, or the first two days of any project. Under roughly 1,000 lines it is usually the right answer. More structure than this, this early, costs time and buys nothing.

It stops working when: you cannot find things any more, or two unrelated parts of the file start needing the same helper with the same name.

wordcount/the whole project
go.modmodule name and Go version
main.gopackage main — where the program starts
count.gopackage main — more of the same program
count_test.gotests for count.go
Every file says `package main`, so nothing needs importing. Tests sit beside the code they test.

Layout 2 — grouped by layer

The next step most people take: a folder per *kind of file*. All the data types together, all the business logic together, all the HTTP handlers together.

This is the layout most frameworks in other languages push you towards, so it feels familiar. It is worth understanding because you will inherit codebases built this way.

The problem shows up at about ten features. Adding one feature means editing five folders, every package ends up importing every other package, and no feature can be understood, or deleted, on its own. You also hit Go import cycles quickly, because models wants something from services and services already imports models.

shop/
go.mod
cmd/runnable programs
api/
main.go
internal/private to this project
models/User, Order, Product — all of them together
user.go
order.go
services/the logic for all of them
user.go
order.go
handlers/the HTTP layer for all of them
user.go
order.go
repository/the SQL for all of them
user.go
order.go
To change how orders work you open four folders. To delete orders you must find its pieces in all four.

Layout 3 — grouped by domain

A folder per *thing your product has*, not per kind of file. Everything about orders lives in the order package: its type, its rules, its SQL, its HTTP handlers, its tests.

This is a common choice for Go services, and what the rest of this track uses. One feature is one folder, so you can read it, test it, hand it to someone else, or delete it without hunting.

Use it for: anything that will grow: a web service, an API, a long-lived tool.

shop/
go.mod
cmd/runnable programs, no logic
api/
main.gowiring only
internal/private to this project
config/reads the environment, once
database/opens the connection pool
user/EVERYTHING about users
model.gothe User type
repository.gothe SQL
service.gothe rules
handler.gothe HTTP layer
service_test.goits tests
order/everything about orders
product/everything about products
migrations/the database schema, as numbered .sql files
To delete the order feature you delete one folder and a few lines of wiring in main.go. That is a good test of any layout.

The three side by side

LayoutBest forFalls apart when
Flat — one packageCLIs, scripts, small libraries, day one of anythingyou can no longer find things (~1,000 lines)
By layer — models/, services/small apps; codebases you inheritabout ten features, or the first import cycle
By domain — user/, order/services and APIs meant to growrarely — but it is overkill for a 200-line tool
Note: Start flat. Move to domain folders when the flat file starts to hurt. Starting at layout 3 for a 200-line program is the same mistake as staying at layout 1 for a 20,000-line one.

The names you will hear, and where each one appears

Those layouts have names, and people use them loosely. The table shows what each name means and which project in this track builds it, so you finish having written each one rather than only read about it.

LayoutAlso calledBuilt inReach for it when
Flatsingle packagePractice 1a CLI, a script, or under ~1,000 lines
Layeredpackage-by-layer, n-tier, standard layoutPractice 2, Project 1 — URL shortenerthe service has one concept
By featurepackage-by-domain, vertical slice, DDD-litePractice 3, Project 2 — Expense trackerseveral concepts, and it will grow — the usual default
Ports and adaptershexagonal, clean, onionProject 3 — Small shopyou expect to swap an external dependency
Note: There is no winner here. The layout should match the size and the shape of the problem, and moving between them is a normal thing to do as a service grows. In an interview, being able to say *why* you picked one matters more than which one you picked.

What every folder means

These names are conventions, not rules, except internal/, which the go tool enforces. Learn them because most Go repositories you open will use some of them.

FolderWhat goes in itWhy it exists
cmd/One subfolder per runnable program, each with its own main.go.A project often ships more than one binary: an API, a worker, a seeder. cmd/api/ and cmd/worker/ keep them apart. Put no logic here, only wiring.
internal/Everything private to this project.The go tool refuses imports of it from outside the project. It is the only folder rule Go enforces, so you get a private API for free.
pkg/Code you intend outsiders to import.The opposite of internal/. Only add it if you are publishing a library. For a normal service it adds a folder level and nothing else, and many teams skip it.
internal/<domain>/One folder per business concept: user/, order/.Keeps one feature in one place. This is the folder you delete when a feature goes away.
internal/config/Reading and checking environment variables.Config is loaded once, at startup, and passed down. Nothing deeper should read the environment.
internal/database/Opening the connection pool, shared database helpers.One place that knows how to connect, so no domain package repeats it.
internal/middleware/Logging, panic recovery, auth checks, rate limiting.Cuts across every request, so it belongs to no single domain.
internal/httpx/ (or web/)Shared HTTP helpers: JSON writing, error-to-status mapping.Stops every handler reinventing "how do I return an error".
migrations/Numbered .sql files that build the database schema.The schema is version-controlled like code, applied by a tool, never by hand.
api/OpenAPI or protobuf definitions.The contract with your clients, kept where they can find it. Optional.
testdata/Fixture files used by tests.The go tool ignores any folder called `testdata`, so nothing in it is compiled.
scripts/Build, deploy and helper scripts.Keeps shell scripts out of the root. Optional.
vendor/Copies of your dependencies.Created by go mod vendor. Mostly used for builds with no network access, or when a team wants every dependency's source in the repository.

internal/ is the one that is real

Everything above is a convention you could ignore. internal/ is not. If a package sits anywhere under a folder named internal, only code rooted at that folder’s parent can import it. Anyone else gets a compile error.

The practical advice: put almost everything in internal/. Move something out only when you have decided, on purpose, that outsiders may depend on it. Once they do, you cannot change it freely.

what internal/ blocks (text diagram, not Go)
github.com/you/shop/internal/user ← lives here
github.com/you/shop/cmd/api ✅ can import it (same module)
github.com/other/thing ❌ compile error, always

Naming packages

  • Short, lowercase, one word. user, order, config. No underscores, no camelCase, no plurals.
  • The package name is part of every call, so avoid repeating it. user.Service reads well; user.UserService stutters. Same for user.New() over user.NewUser().
  • Name it for what it provides, not what it contains. store not structs.
  • Never create `utils`, `helpers`, `common`, `base` or `shared`. They have no meaning, so everything drifts into them and they end up importing everything, which causes import cycles. If you cannot name the package for what it does, you have not decided what it does.

How to choose, in one line

One program and under a thousand lines: flat. A service you expect to grow: one folder per domain, everything under internal/, main.go doing nothing but wiring. Both are easy to defend in a review.

The short version
  • A package is a folder; capitalisation decides what is visible outside it. A module is a project with a go.mod.
  • Three layouts: flat for small things, grouped by layer (common but ages badly), grouped by domain for anything that grows.
  • cmd/ holds runnable programs and no logic. internal/ is private and the compiler enforces it.
  • Never make a utils or common package, and never let a package name stutter.
Remember: The layout should match the size of the problem. Start flat, move to one folder per feature when the flat version starts to hurt.
Build this before moving on
Take any small program you have written and lay it out all three ways. Notice how much work the layer version creates for one small change, and how little the flat version needs.
Then try these in your own editor
  1. 1Open two Go projects on GitHub and work out which of the three layouts each one uses.
  2. 2Take a small program you have written and lay it out all three ways. Notice how much work the layer version creates for one small change.
  3. 3Put a package under internal/ and try to import it from a second module. Read the compiler error.
  4. 4Find a utils package in a project you have worked on and write down what is actually in it. That list is the packages it should have been.
Check yourselfTake the Practice projects test