Go API Structure
The runnable example/ module — in this skill's repo, not the installed skill — declares
go 1.27.0. Advice that only holds from a particular release carries an inline version gate at
the point it is given; everything without one applies to any supported toolchain.
The rule everything else follows
Directories are navigation. The import graph is the architecture.
Moving a file into internal/core/ does not decouple it from Postgres; deleting its
import "database/sql" does. Decide which way imports point first, then decide where files
go. Get the first wrong and no layout rescues it.
The words this skill uses
Four role names — Domain, Adapter, Transport, Capability — each mapped to the package in
example/ that plays it, plus a wire type and the mechanism that inverts a dependency. The roles
matter more than the definitions: every rule below is stated in terms of which role may import
which.
| Term |
What it means here |
| Domain |
Business rules — "an email must be unique". Knows nothing about HTTP or databases. internal/accounts. |
| Adapter |
Talks to something outside the process — a database, Stripe, S3. internal/sqlstore. |
| Transport |
Speaks the wire protocol — handlers, JSON, status codes. internal/httpapi. |
| Capability |
In-process machinery that is neither: a worker pool, a password hasher. Named for what it does. internal/jobs. |
| DTO |
Data Transfer Object — a struct that exists only to shape data on the wire, carrying the json: tags a domain type must not. Separate from the domain type so the API's shape and the business model change independently. |
| Consumer-declared interface |
An interface written in the package that calls it, not the one that implements it. This is the mechanism that points an arrow the other way. |
Bounded context — one coherent slice of the business (accounts, billing) — decides where
one package ends and the next begins. Backpressure is defined where it is used, in
references/concurrency.md.
Why "the import graph is the architecture" is a literal claim
Take a working service and rename the directory holding its business logic:
before: httpapi ──▶ accounts ◀── sqlstore
after: httpapi ──▶ core/domain/entities ◀── sqlstore
Every importing file had to be edited and the build broke until they were. Yet the dependencies
are identical — the same two arrows, pointing the same way. If the business logic imported a
database driver before the rename, it still does.
That is the whole point. Coupling is an arrow existing, and the only way to remove an arrow
is to delete the import line. Renaming relabels a box. A directory layout, on its own, cannot
decouple anything — which is why this skill decides arrow direction first and folder names
second.
(A related consequence: a directory's name and its package name are independent in Go. They
match by convention, not by rule.)
Step 0 — pick the tier before drawing folders
Over-structuring a small service costs more than under-structuring, because every later
change pays the ceremony tax.
| Tier |
When |
Layout |
| 1 |
One binary, <10 endpoints, no swappable dependencies |
main.go plus 2–4 packages under internal/. No cmd/. |
| 2 (default) |
A real service: a DB, some clients, one or two binaries |
The canonical layout below. |
| 3 |
Several bounded contexts or many binaries |
Tier 2 with one domain package per context, one cmd/ per binary. Separate modules only when release cadences diverge. |
State the tier before proposing a layout. Unsure between 1 and 2? Pick 1 — promoting is a
git mv, demoting never happens.
Dependency direction (non-negotiable)
cmd/api ────────────────────────────────────┐ wires everything, imports everyone
│ │
▼ ▼
internal/httpapi ────▶ internal/accounts ◀──── internal/sqlstore
(transport) (domain: types, rules, (adapter: satisfies the
and the interfaces interfaces the domain
it needs) declared)
Arrows point inward. The domain imports neither transport nor adapters — it declares the
interfaces both are shaped around. So internal/accounts must not import net/http,
database/sql, a driver, or a vendor SDK. If it does, the boundary has already leaked.
The default layout (tier 2)
user-service/
├── cmd/
│ ├── api/main.go # load config, wire, serve, shut down
│ └── worker/main.go # shares internal/, different entry point
├── internal/
│ ├── config/ # env/flags → a typed Config
│ ├── accounts/ # DOMAIN: entities, rules, the interfaces it needs
│ │ ├── accounts.go # User, errors, Store + Hasher interfaces
│ │ ├── service.go # Register, Authenticate, ChangeEmail …
│ │ └── service_test.go # in-memory fakes, no DB
│ ├── billing/ # second bounded context, same shape
│ ├── sqlstore/ # ADAPTER: implements accounts.Store, billing.Store
│ ├── payments/ # ADAPTER: implements billing.PaymentGateway (wraps stripe-go)
│ ├── jobs/ # CAPABILITY: bounded worker pool (see concurrency.md)
│ └── httpapi/ # TRANSPORT: router, middleware, handlers, wire DTOs
│ ├── server.go
│ ├── accounts.go # handlers + their request/response types
│ └── middleware.go
├── migrations/
├── openapi.yaml
├── Makefile
└── go.mod
Two things commonly copied layouts get wrong:
- Infrastructure goes inside
internal/. An infrastructure/ directory at the repo
root makes DB repos and clients importable by anything depending on your module, which
defeats the reason internal/ was chosen.
- Adapters group by technology, not feature. You swap one database for another, not "the
user half of the database." Domain code groups by feature; adapters group by external
system. Confine the driver-specific parts (error codes, placeholder syntax) to one place so
the swap stays cheap.
Interfaces: the default posture
Every boundary that leaves the process gets an interface, declared by the caller — DB,
HTTP client, queue, cache, object store, mailer, shell.
Nothing else does. In-process logic is tested by calling it. An interface with one
implementation and no test fake is indirection charging rent.
Non-determinism is the exception that is not an interface. Clock, randomness and ID
generation never leave the process, so inject them as plain function values —
now func() time.Time, newID func() string — not a Clock interface, which would be a
one-method interface with exactly one production implementation.
- Declare the interface in the package that consumes it.
accounts declares Store;
sqlstore imports accounts and satisfies it. This is what inverts the dependency — the
folder move does not.
- Only the methods that caller calls. A 3-method interface is an interface; a 20-method
Repository is the concrete type with extra steps.
- Assert satisfaction at compile time where it is implemented:
var _ accounts.Store = (*AccountStore)(nil).
Accept interfaces, return structs — constructors return *accounts.Service.
Worked examples, fakes, and cycle-breaking: references/interfaces.md.
Where does this file go?
| What is being written |
Directory |
Package |
| Entity or value type with business rules |
internal/<domain>/ |
accounts |
| Interface for something the domain needs |
same package as its consumer |
accounts |
| A business workflow / use case |
internal/<domain>/service.go, as a method |
accounts |
| SQL, queries, row scanning, DB structs |
internal/sqlstore/ |
sqlstore |
| HTTP handler, decode, encode, status mapping |
internal/httpapi/ |
httpapi |
| Request/response DTO |
internal/httpapi/, beside its handler |
httpapi |
| Third-party API client (an adapter — it leaves the process) |
internal/<capability>/ |
payments |
| Queue producer/consumer |
internal/eventbus/ |
eventbus |
| Background work, bounded concurrency, a job queue |
internal/jobs/ |
jobs |
| Password hashing, crypto, other in-process capabilities |
internal/<capability>/ |
pwhash |
| Env parsing, flags, defaults |
internal/config/ |
config |
| Wiring, lifecycle, graceful shutdown |
cmd/<binary>/main.go |
main |
| An atomic write across two stores |
domain declares Atomic, adapter owns the tx |
accounts + sqlstore |
| Something two domains both need |
first try moving the interface to the consumer; a shared domain package is the last resort, and never shared/ |
— |
| Genuinely reusable outside this repo |
a separate published module |
— |
A use case is a method on a service, not a package. One package per endpoint turns a
20-endpoint service into 20+ packages, each exporting a DTO the next one imports.
Package naming
- Lowercase, one word, no underscores, no camelCase.
useCases/, registerUser/,
getProfile/ are legal identifiers but violate the naming convention every Go reader and
linter expects. The package name comes from the package clause, not the directory — but
they are conventionally identical, so a useCases/ directory produces package useCases
in practice. State this as convention, not as a language rule.
- Name the package for the bounded context, not the entity, so call sites don't stutter:
accounts.User, not user.User.
- Never
util, common, helpers, shared, base, misc, or an app-wide models. They
have no boundary, so everything drifts into them.
- Name an adapter for the external system it wraps — that is the thing you swap — but use
the capability whenever that name would collide with its own client library's package
(
cache not redis, sqlstore not sqlite). Collision is the common case, so capability
names are the common answer. Full list and reasoning in
references/layout.md.
- Avoid
cmd/http/; name binaries for what they are (cmd/api/), not their transport.
golang-standards/project-layout is not a standard — name it explicitly when you reject it,
because an unnamed rule loses to a repo called "Standard Go Project Layout". Why it carries that
authority, and why pkg/ earns nothing that internal/ does not,
is in references/layout.md.
Gates
Run these rather than trying to recall the list below:
gofmt -l . # any output at all is a failure
go vet ./...
golangci-lint run
go test ./... -race -shuffle=on
govulncheck ./...
golangci-lint on its defaults enforces none of the rules argued for here, so a green run says
nothing about them. example/.golangci.yml is the config this skill
ships — every linter in it records the red flag it catches and why it is on.
-race and -shuffle=on catch the two things a green suite hides: a data race no single test
observes, and a test that only passes because an earlier one left state behind.
govulncheck reports only vulnerabilities your code actually reaches, and most of what it
returns is standard library. A stdlib finding means "upgrade your Go", which is exactly the
signal the gate exists to give. Against example/ it currently prints No vulnerabilities found. — that is the expected output, not a sign the tool did nothing.
The red flags below are what none of these can see.
Red flags
- A domain package importing
database/sql, net/http, a driver, or a vendor SDK
- An interface declared in the same package as its only implementation
- An interface with one implementation and no test fake
pkg/ at the repo root, or a package named utils/common/shared
- An app-wide
models/ every other package imports
- A
dependencies.go past ~150 lines, or returning a struct of 30 fields
- A request
context.Context stored in a struct, or a ctx that is not the first parameter
(a long-lived component holding its own lifecycle context, cancelled by its Shutdown, is the
documented exception — see references/concurrency.md)
- A
Query/Exec where a QueryContext/ExecContext exists — cancellation silently dropped
context.WithTimeout in a leaf function, overriding a budget the edge already set — the
exception is deliberately bounding one outbound call so it cannot eat the whole budget
(see references/layout.md)
context.WithValue with a bare string key, or used to pass a dependency
- A goroutine outliving its request while still holding the request's
ctx — see
references/concurrency.md for the bounded alternative
- An import cycle "fixed" by inventing a third package for the shared types — the cycle means
the interface is declared on the wrong side
- Domain types carrying
json: or db: tags — wire and table leaking inward
- A handler decoding a request body with no
http.MaxBytesReader in front of it — one client
can make the process allocate until it dies; see references/transport.md
- A package-level logger — global mutable state no test can substitute, so log assertions go
order-dependent the moment tests run in parallel
- A
*slog.Logger field on a domain struct — a dependency the domain needs to compute nothing,
carried by every constructor and every test (see references/transport.md)
- An HTTP server with no panic-recovery middleware — one nil dereference in one handler takes
the whole process down
- Request/response types at package scope in the transport package — the second handler reuses
one, and then a field added for endpoint A changes endpoint B's wire contract with no diff at
the site that broke it
- A readiness endpoint that returns 200 unconditionally — it decides load-balancer routing
without consulting anything, so a broken instance keeps being sent traffic
- A test suite that is all unit tests — ten passing functions do not mean the flow works;
see the functional tests in
example/
- A test that has never been watched fail
Out of scope
Named so the omission reads as a decision rather than an oversight. Each of these has real
disagreement behind it, and a skill that improvised an answer would be asserting a preference
it has not argued for.
- The database query layer. Whether to reach for
sqlc, hand-written SQL, or an ORM. What
this skill does constrain is where the answer lives: behind a consumer-declared interface, in
an adapter package, with driver-specific error decoding confined to one function — see
references/interfaces.md and references/layout.md.
- Migration tooling.
golang-migrate, atlas, goose.
references/layout.md states the
one rule that is not a tooling preference: do not auto-migrate from the API binary at startup.
- API contracts and versioning. OpenAPI,
oapi-codegen, spec-first versus code-first, and
how to version an endpoint. references/transport.md covers what a handler does with a
request, not how its schema is published or evolved.
References
| File |
When to read |
references/interfaces.md |
Anything crossing a process boundary; deciding whether something deserves an interface; writing fakes; breaking an import cycle |
references/transport.md |
Adding or reviewing an endpoint: routing, handlers, decoding and validating a body, where middleware goes and in what order, panic recovery, request IDs, logging at the edge, tracing, liveness vs readiness, rejecting oversized or hostile input |
example/ |
A runnable version of this whole service — cd example && go test ./... -race -shuffle=on && golangci-lint run, against the .golangci.yml it ships. Read functional/flow_test.go for what good tests look like here |
references/concurrency.md |
Running work in the background or in parallel: worker pools, job queues, capping how many run at once, backpressure, draining on shutdown, goroutine leaks, errgroup |
references/layout.md |
Standing up or restructuring a service: per-directory contracts, adapter naming, tier growth, main.go wiring, config, graceful shutdown, context deadlines/cancellation/values, test placement, and why there is no pkg/ |
1---2name: go-api-structure3description: Structure Go API and service codebases — package layout, interface-driven dependency direction, and the HTTP edge. Use when starting a Go service, adding a feature or endpoint, deciding which package a file or type belongs in, resolving an import cycle, reviewing Go layout in a PR, or asking "how should I structure my Go project". Also before writing Go that touches a database, HTTP client, cache, queue, or clock, so it lands behind a consumer-declared interface. Covers routing, handlers, middleware placement and order, panic recovery, decoding and validating a JSON body, rejecting oversized input, where wire DTOs live, structured logging with slog, liveness versus readiness health checks; context deadlines, cancellation, ctx-first, WithValue keys; config loading; testing — where tests belong, functional tests, fakes, synctest, race and goroutine-leak detection; and concurrency — worker pools, job queues, bounding concurrency, backpressure, graceful shutdown.4---56# Go API Structure78The runnable `example/` module — in this skill's repo, not the installed skill — declares9`go 1.27.0`. Advice that only holds from a particular release carries an inline version gate at10the point it is given; everything without one applies to any supported toolchain.1112## The rule everything else follows1314Directories are navigation. **The import graph is the architecture.**1516Moving a file into `internal/core/` does not decouple it from Postgres; deleting its17`import "database/sql"` does. Decide which way imports point first, then decide where files18go. Get the first wrong and no layout rescues it.1920## The words this skill uses2122Four role names — Domain, Adapter, Transport, Capability — each mapped to the package in23`example/` that plays it, plus a wire type and the mechanism that inverts a dependency. The roles24matter more than the definitions: every rule below is stated in terms of which role may import25which.2627| Term | What it means here |28|---|---|29| **Domain** | Business rules — "an email must be unique". Knows nothing about HTTP or databases. `internal/accounts`. |30| **Adapter** | Talks to something outside the process — a database, Stripe, S3. `internal/sqlstore`. |31| **Transport** | Speaks the wire protocol — handlers, JSON, status codes. `internal/httpapi`. |32| **Capability** | In-process machinery that is neither: a worker pool, a password hasher. Named for what it does. `internal/jobs`. |33| **DTO** | Data Transfer Object — a struct that exists only to shape data on the wire, carrying the `json:` tags a domain type must not. Separate from the domain type so the API's shape and the business model change independently. |34| **Consumer-declared interface** | An interface written in the package that *calls* it, not the one that implements it. This is the mechanism that points an arrow the other way. |3536**Bounded context** — one coherent slice of the business (`accounts`, `billing`) — decides where37one package ends and the next begins. **Backpressure** is defined where it is used, in38[`references/concurrency.md`](references/concurrency.md#the-problem-this-solves).3940### Why "the import graph is the architecture" is a literal claim4142Take a working service and rename the directory holding its business logic:4344```45before: httpapi ──▶ accounts ◀── sqlstore46after: httpapi ──▶ core/domain/entities ◀── sqlstore47```4849Every importing file had to be edited and the build broke until they were. Yet the dependencies50are identical — the same two arrows, pointing the same way. If the business logic imported a51database driver before the rename, it still does.5253That is the whole point. **Coupling is an arrow existing**, and the only way to remove an arrow54is to delete the `import` line. Renaming relabels a box. A directory layout, on its own, cannot55decouple anything — which is why this skill decides arrow direction first and folder names56second.5758(A related consequence: a directory's name and its package name are independent in Go. They59match by convention, not by rule.)6061## Step 0 — pick the tier before drawing folders6263Over-structuring a small service costs more than under-structuring, because every later64change pays the ceremony tax.6566| Tier | When | Layout |67|------|------|--------|68| **1** | One binary, <10 endpoints, no swappable dependencies | `main.go` plus 2–4 packages under `internal/`. No `cmd/`. |69| **2** (default) | A real service: a DB, some clients, one or two binaries | The canonical layout below. |70| **3** | Several bounded contexts or many binaries | Tier 2 with one domain package per context, one `cmd/` per binary. Separate modules only when release cadences diverge. |7172State the tier before proposing a layout. Unsure between 1 and 2? Pick 1 — promoting is a73`git mv`, demoting never happens.7475## Dependency direction (non-negotiable)7677```78 cmd/api ────────────────────────────────────┐ wires everything, imports everyone79 │ │80 ▼ ▼81internal/httpapi ────▶ internal/accounts ◀──── internal/sqlstore82 (transport) (domain: types, rules, (adapter: satisfies the83 and the interfaces interfaces the domain84 it needs) declared)85```8687Arrows point inward. The domain imports neither transport nor adapters — it declares the88interfaces both are shaped around. So `internal/accounts` must not import `net/http`,89`database/sql`, a driver, or a vendor SDK. If it does, the boundary has already leaked.9091## The default layout (tier 2)9293```94user-service/95├── cmd/96│ ├── api/main.go # load config, wire, serve, shut down97│ └── worker/main.go # shares internal/, different entry point98├── internal/99│ ├── config/ # env/flags → a typed Config100│ ├── accounts/ # DOMAIN: entities, rules, the interfaces it needs101│ │ ├── accounts.go # User, errors, Store + Hasher interfaces102│ │ ├── service.go # Register, Authenticate, ChangeEmail …103│ │ └── service_test.go # in-memory fakes, no DB104│ ├── billing/ # second bounded context, same shape105│ ├── sqlstore/ # ADAPTER: implements accounts.Store, billing.Store106│ ├── payments/ # ADAPTER: implements billing.PaymentGateway (wraps stripe-go)107│ ├── jobs/ # CAPABILITY: bounded worker pool (see concurrency.md)108│ └── httpapi/ # TRANSPORT: router, middleware, handlers, wire DTOs109│ ├── server.go110│ ├── accounts.go # handlers + their request/response types111│ └── middleware.go112├── migrations/113├── openapi.yaml114├── Makefile115└── go.mod116```117118Two things commonly copied layouts get wrong:119120- **Infrastructure goes inside `internal/`.** An `infrastructure/` directory at the repo121 root makes DB repos and clients importable by anything depending on your module, which122 defeats the reason `internal/` was chosen.123- **Adapters group by technology, not feature.** You swap one database for another, not "the124 user half of the database." Domain code groups by feature; adapters group by external125 system. Confine the driver-specific parts (error codes, placeholder syntax) to one place so126 the swap stays cheap.127128## Interfaces: the default posture129130**Every boundary that leaves the process gets an interface, declared by the caller** — DB,131HTTP client, queue, cache, object store, mailer, shell.132133**Nothing else does.** In-process logic is tested by calling it. An interface with one134implementation and no test fake is indirection charging rent.135136**Non-determinism is the exception that is not an interface.** Clock, randomness and ID137generation never leave the process, so inject them as plain function values —138`now func() time.Time`, `newID func() string` — not a `Clock` interface, which would be a139one-method interface with exactly one production implementation.1401411. **Declare the interface in the package that consumes it.** `accounts` declares `Store`;142 `sqlstore` imports `accounts` and satisfies it. This is what inverts the dependency — the143 folder move does not.1442. **Only the methods that caller calls.** A 3-method interface is an interface; a 20-method145 `Repository` is the concrete type with extra steps.1463. **Assert satisfaction at compile time** where it is implemented:147 `var _ accounts.Store = (*AccountStore)(nil)`.148149Accept interfaces, return structs — constructors return `*accounts.Service`.150151Worked examples, fakes, and cycle-breaking: `references/interfaces.md`.152153## Where does this file go?154155| What is being written | Directory | Package |156|---|---|---|157| Entity or value type with business rules | `internal/<domain>/` | `accounts` |158| Interface for something the domain needs | same package as its **consumer** | `accounts` |159| A business workflow / use case | `internal/<domain>/service.go`, as a method | `accounts` |160| SQL, queries, row scanning, DB structs | `internal/sqlstore/` | `sqlstore` |161| HTTP handler, decode, encode, status mapping | `internal/httpapi/` | `httpapi` |162| Request/response DTO | `internal/httpapi/`, beside its handler | `httpapi` |163| Third-party API client (an adapter — it leaves the process) | `internal/<capability>/` | `payments` |164| Queue producer/consumer | `internal/eventbus/` | `eventbus` |165| Background work, bounded concurrency, a job queue | `internal/jobs/` | `jobs` |166| Password hashing, crypto, other in-process capabilities | `internal/<capability>/` | `pwhash` |167| Env parsing, flags, defaults | `internal/config/` | `config` |168| Wiring, lifecycle, graceful shutdown | `cmd/<binary>/main.go` | `main` |169| An atomic write across two stores | domain declares `Atomic`, adapter owns the tx | `accounts` + `sqlstore` |170| Something two domains both need | first try moving the interface to the consumer; a shared domain package is the last resort, and never `shared/` | — |171| Genuinely reusable outside this repo | a separate published module | — |172173A use case is a **method on a service, not a package**. One package per endpoint turns a17420-endpoint service into 20+ packages, each exporting a DTO the next one imports.175176## Package naming177178- Lowercase, one word, no underscores, **no camelCase**. `useCases/`, `registerUser/`,179 `getProfile/` are legal identifiers but violate the naming convention every Go reader and180 linter expects. The package name comes from the `package` clause, not the directory — but181 they are conventionally identical, so a `useCases/` directory produces `package useCases`182 in practice. State this as convention, not as a language rule.183- **Name the package for the bounded context, not the entity**, so call sites don't stutter:184 `accounts.User`, not `user.User`.185- Never `util`, `common`, `helpers`, `shared`, `base`, `misc`, or an app-wide `models`. They186 have no boundary, so everything drifts into them.187- Name an adapter for **the external system it wraps** — that is the thing you swap — but use188 the **capability** whenever that name would collide with its own client library's package189 (`cache` not `redis`, `sqlstore` not `sqlite`). Collision is the common case, so capability190 names are the common answer. Full list and reasoning in191 [`references/layout.md`](references/layout.md#internaladapter--sqlstore-cache-eventbus-objectstore-payments).192- Avoid `cmd/http/`; name binaries for what they are (`cmd/api/`), not their transport.193194**`golang-standards/project-layout` is not a standard** — name it explicitly when you reject it,195because an unnamed rule loses to a repo called "Standard Go Project Layout". Why it carries that196authority, and why `pkg/` earns nothing that `internal/` does not,197is in [`references/layout.md`](references/layout.md#why-there-is-no-pkg).198199## Gates200201Run these rather than trying to recall the list below:202203```bash204gofmt -l . # any output at all is a failure205go vet ./...206golangci-lint run207go test ./... -race -shuffle=on208govulncheck ./...209```210211`golangci-lint` on its defaults enforces none of the rules argued for here, so a green run says212nothing about them. [`example/.golangci.yml`](example/.golangci.yml) is the config this skill213ships — every linter in it records the red flag it catches and why it is on.214215`-race` and `-shuffle=on` catch the two things a green suite hides: a data race no single test216observes, and a test that only passes because an earlier one left state behind.217218`govulncheck` reports only vulnerabilities your code actually reaches, and most of what it219returns is standard library. A stdlib finding means "upgrade your Go", which is exactly the220signal the gate exists to give. Against `example/` it currently prints `No vulnerabilities221found.` — that is the expected output, not a sign the tool did nothing.222223The red flags below are what none of these can see.224225## Red flags226227- A domain package importing `database/sql`, `net/http`, a driver, or a vendor SDK228- An interface declared in the same package as its only implementation229- An interface with one implementation and no test fake230- `pkg/` at the repo root, or a package named `utils`/`common`/`shared`231- An app-wide `models/` every other package imports232- A `dependencies.go` past ~150 lines, or returning a struct of 30 fields233- A **request** `context.Context` stored in a struct, or a ctx that is not the first parameter234 (a long-lived component holding its own lifecycle context, cancelled by its `Shutdown`, is the235 documented exception — see `references/concurrency.md`)236- A `Query`/`Exec` where a `QueryContext`/`ExecContext` exists — cancellation silently dropped237- `context.WithTimeout` in a leaf function, overriding a budget the edge already set — the238 exception is deliberately bounding one outbound call so it cannot eat the whole budget239 (see [`references/layout.md`](references/layout.md#context-deadlines-cancellation-values))240- `context.WithValue` with a bare `string` key, or used to pass a dependency241- A goroutine outliving its request while still holding the request's `ctx` — see242 `references/concurrency.md` for the bounded alternative243- An import cycle "fixed" by inventing a third package for the shared types — the cycle means244 the interface is declared on the wrong side245- Domain types carrying `json:` or `db:` tags — wire and table leaking inward246- A handler decoding a request body with no `http.MaxBytesReader` in front of it — one client247 can make the process allocate until it dies; see `references/transport.md`248- A package-level logger — global mutable state no test can substitute, so log assertions go249 order-dependent the moment tests run in parallel250- A `*slog.Logger` field on a domain struct — a dependency the domain needs to compute nothing,251 carried by every constructor and every test (see `references/transport.md`)252- An HTTP server with no panic-recovery middleware — one nil dereference in one handler takes253 the whole process down254- Request/response types at package scope in the transport package — the second handler reuses255 one, and then a field added for endpoint A changes endpoint B's wire contract with no diff at256 the site that broke it257- A readiness endpoint that returns 200 unconditionally — it decides load-balancer routing258 without consulting anything, so a broken instance keeps being sent traffic259- A test suite that is all unit tests — ten passing functions do not mean the flow works;260 see the functional tests in `example/`261- A test that has never been watched fail262263## Out of scope264265Named so the omission reads as a decision rather than an oversight. Each of these has real266disagreement behind it, and a skill that improvised an answer would be asserting a preference267it has not argued for.268269- **The database query layer.** Whether to reach for `sqlc`, hand-written SQL, or an ORM. What270 this skill does constrain is where the answer lives: behind a consumer-declared interface, in271 an adapter package, with driver-specific error decoding confined to one function — see272 `references/interfaces.md` and `references/layout.md`.273- **Migration tooling.** `golang-migrate`, `atlas`, `goose`.274 [`references/layout.md`](references/layout.md#migrations) states the275 one rule that is not a tooling preference: do not auto-migrate from the API binary at startup.276- **API contracts and versioning.** OpenAPI, `oapi-codegen`, spec-first versus code-first, and277 how to version an endpoint. `references/transport.md` covers what a handler does with a278 request, not how its schema is published or evolved.279280## References281282| File | When to read |283|------|-------------|284| `references/interfaces.md` | Anything crossing a process boundary; deciding whether something deserves an interface; writing fakes; breaking an import cycle |285| `references/transport.md` | Adding or reviewing an endpoint: routing, handlers, decoding and validating a body, where middleware goes and in what order, panic recovery, request IDs, logging at the edge, tracing, liveness vs readiness, rejecting oversized or hostile input |286| `example/` | A runnable version of this whole service — `cd example && go test ./... -race -shuffle=on && golangci-lint run`, against the `.golangci.yml` it ships. Read `functional/flow_test.go` for what good tests look like here |287| `references/concurrency.md` | Running work in the background or in parallel: worker pools, job queues, capping how many run at once, backpressure, draining on shutdown, goroutine leaks, `errgroup` |288| `references/layout.md` | Standing up or restructuring a service: per-directory contracts, adapter naming, tier growth, `main.go` wiring, config, graceful shutdown, context deadlines/cancellation/values, test placement, and why there is no `pkg/` |