Go Best Practices

Code organisation

Structural conventions that make codebases navigable and dependencies explicit .

Layers and dependencies

The codebase is split into four layers , each with a clear responsibility and strict dependency rules :

cmd/server/           APP        bootstrap , wiring , configuration
internal/api/         API        handlers , proto mapping , routing
internal/domain/      DOMAIN     business logic , transactions , canonical store
internal/outbox/      OUTBOX     async event processing , data projections

Dependencies flow in one direction :

  • pkg/ depends on nothing . Shared , business-agnostic utilities .
  • gen/ depends on nothing internal . Generated code from protobuf and sqlc .
  • internal/domain/ depends on gen/db/ and pkg/ . Owns the business logic and the canonical store .
  • internal/outbox/ depends on internal/domain/ and pkg/ . Reacts to domain events .
  • internal/api/ depends on internal/domain/ , gen/sdk/ , and pkg/ . Maps between proto types and domain types .
  • cmd/server/ imports everything . It is the only layer that wires all the pieces together .

No layer reaches down . internal/domain/ never imports internal/api/ . internal/api/ never imports internal/outbox/ . If you break this rule , the agent has drifted .

Interface-first

Every package exposes an interface as its public API . Structs are unexported . Constructors return the interface type . This makes dependencies mockable for testing and swappable without touching consumers .

Dependencies struct

Each layer defines an exported Dependencies struct . Constructors accept it as the single parameter . No global state , no hidden wiring , no init functions . Everything a component needs is visible in its constructor signature .

File naming that signals intent

route_<rpc>.go in the API layer . op_<operation>.go in the domain layer . event_<concern>.go in the outbox layer . You can tell what a file does and which layer it belongs to without opening it .

Business-agnostic pkg/

The pkg/ directory contains only business-agnostic code : caching , error mapping , interceptors , outbox interfaces , migration tooling . Because it depends on nothing internal , it can be extracted into a shared module when it stabilises . The convention makes extraction a mechanical step , not a refactoring project .