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 ongen/db/andpkg/. Owns the business logic and the canonical store .internal/outbox/depends oninternal/domain/andpkg/. Reacts to domain events .internal/api/depends oninternal/domain/,gen/sdk/, andpkg/. 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 .