# Go Backend — Hexagonal Architecture (Ports & Adapters) > Canonical boilerplate for ALL Go backends by André Bassi. Hexagonal is INVIOLABLE: `adapters/ -> ports/ -> domain/`, never reverse. No ORM, ever. Point an LLM here and it scaffolds/extends a Go backend exactly in this pattern. ## Hard Rules 1. Direction: `adapters/ -> ports/ -> domain/`. domain/ NEVER imports external libs (no pgx, no http, no SDKs). 2. All communication with the outside world goes through an interface declared in `ports/`. 3. Dependency injection happens ONLY in `cmd/*/main.go`. 4. No ORM. pgx/v5 + interface-driven `DBTX`. 5. Router is chi (go-chi/chi/v5). Never gin, echo, gorilla/mux, fiber. 6. HTTP client is stdlib `net/http`. Never resty/req/etc. 7. Logging is stdlib `log/slog`, JSON structured. 8. Errors: wrap with `fmt.Errorf("context: %w", err)`; sentinel errors in domain. 9. Config: env vars loaded + validated at boot in `internal/config` — fail fast on missing. Guard dangerous flags (e.g. reject `DEV_AUTH_DISABLED=true` when `APP_ENV=production`). 10. Identifiers English-only. ## Canonical Layout backend/ ├── cmd/ │ ├── server/main.go # HTTP entrypoint — chi router, ALL DI wiring here │ ├── worker/main.go # Temporal worker entrypoint (if workflows) │ └── migrate/main.go # migrations runner (if not Supabase CLI) ├── domain/ │ ├── entity/ # pure entities, zero external deps │ └── service/ # business logic services (pure, mockable) ├── ports/ │ ├── inbound/ # driving interfaces: SessionManager, WebhookValidator... │ └── outbound/ # driven interfaces: XxxRepository, Transcriber, │ # Summarizer, Messenger, EmailNotifier, │ # WorkflowEngine, DBTX ├── adapters/ │ ├── driven/ # outbound impls — ONE FOLDER PER EXTERNAL SERVICE │ │ ├── postgres/ # pgx/v5 repos: device_repo.go, tenant_repo.go │ │ ├── temporal/ # client + workflows/ + activities/ │ │ ├── stripe/ # native API calls, NO SDK │ │ ├── groq/ # STT │ │ ├── openai/ # fallback transcriber │ │ ├── openrouter/ # LLM summarizer │ │ ├── email/ # Resend │ │ ├── sse/ # Server-Sent Events │ │ ├── whatsmeow/ # WhatsApp session manager │ │ └── health/ # health checks │ └── driving/ │ └── http/ # chi routes + handlers + middleware ├── internal/ │ ├── config/ # env loading + validation, fail fast │ └── observability/ # Sentry + slog middleware ├── pkg/ # optional reusable helpers ├── testdata/ # fixtures └── go.mod # module , go 1.26 ## Standard Dependencies github.com/go-chi/chi/v5 # router — the only one allowed github.com/jackc/pgx/v5 # postgres driver — no ORM on top github.com/pashagolub/pgxmock/v4 # DB mocking against outbound.DBTX github.com/stretchr/testify # assertions go.temporal.io/sdk # durable workflows (when needed) go.mau.fi/whatsmeow # WhatsApp multi-device (when needed) go.mod skeleton: module go 1.26 require ( github.com/go-chi/chi/v5 v5.2.x github.com/jackc/pgx/v5 v5.7.x github.com/pashagolub/pgxmock/v4 v4.9.x github.com/stretchr/testify v1.x ) ## Ports Naming - Outbound repositories: `XxxRepository` (DeviceRepository, TenantRepository). - Capabilities as nouns: `Transcriber`, `Summarizer`, `Messenger`, `EmailNotifier`, `WorkflowEngine`. - DB abstraction: `DBTX` interface in `ports/outbound` (subset of pgx: Exec, Query, QueryRow) — this is what makes pgxmock work with zero real DB. - Package names: lowercase, no underscores. - Adapter folder name = external service name (`postgres/`, `stripe/`, `groq/`). ## DBTX Pattern (the no-ORM testability core) // ports/outbound/dbtx.go type DBTX interface { Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row } - Repos receive `DBTX`, not `*pgxpool.Pool` — production injects pool, tests inject pgxmock. - SQL lives in the repo files, plain and explicit. No query builders. ## Temporal (when workflows needed) - `adapters/driven/temporal/workflows/` + `adapters/driven/temporal/activities/`. - Workflows: deterministic only — no time.Now(), no rand, no I/O; all side effects in activities. - Worker is separate binary: `cmd/worker/main.go`, separate Fly app. - Tests via `go.temporal.io/sdk/testsuite`. ## Testing - Domain: 100% coverage. Pure unit tests, table-driven, fixtures in `testdata/`. - Adapters: >90%. postgres via pgxmock/v4; HTTP handlers via httptest; Temporal via testsuite. - CI/Taskfile BLOCKS if coverage < 90% in any package. - Zero flaky tests tolerated. - Commands: GOWORK=off go test ./... -cover -race -v GOWORK=off go test ./domain/service -run TestName -v GOWORK=off go test ./adapters/driven/postgres -cover -race -v ## main.go Wiring Pattern func main() { cfg := config.Load() // fail fast pool := postgres.NewPool(cfg.DatabaseURL) deviceRepo := postgres.NewDeviceRepo(pool) // adapter implements port svc := service.NewDeviceService(deviceRepo) // domain receives port h := http.NewHandler(svc) // driving adapter r := chi.NewRouter() h.Mount(r) slog.Info("listening", "port", cfg.Port) http.ListenAndServe(":"+cfg.Port, r) } ## Build - Static binary: `CGO_ENABLED=0 go build -ldflags "-X main.Version=${VERSION}" -o server ./cmd/server` - Version from `scripts/version.sh` (commit count semver). - Final Docker image: `chainguard/static:latest` (see llms-devops.txt).