Opinionated Go microservice template with clean architecture, samber/do DI, OpenTelemetry, Prometheus metrics, and an example CRUD feature ready to be deleted.
- Go 1.27+
- Docker/Podman
- Task
- PostgreSQL 18+ (via container)
- Valkey 9+ (via container)
cp .env.example .env
docker compose up -d postgres valkey
task migrate-up
task runThe server listens on 0.0.0.0:8080 for HTTP and 0.0.0.0:50051 for gRPC.
zercle-go-template/
├── .agents/
│ ├── AGENTS.md
│ └── plans/ # spec/canvas/state per task
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ └── ci.yml
├── api/
│ ├── pb/example/v1/ # generated protobuf Go code (task proto)
│ └── proto/example/v1/
│ └── example.proto
├── bin/ # build output (ignored)
├── cmd/
│ ├── migrate/main.go # migration runner
│ └── server/main.go # entry point: loads config, delegates to internal/app
├── deployments/
│ ├── kustomize/
│ │ ├── base/
│ │ └── overlays/
│ └── observability/ # otel-collector + prometheus configs
├── internal/
│ ├── app/ # reusable composition root (DI wiring, app.Run)
│ ├── architecture_test.go # executable dependency gates (runs in task test)
│ ├── features/
│ │ └── example/ # STUB FEATURE — delete to start
│ │ ├── domain/ # entities + sentinel errors (stdlib + uuid only)
│ │ ├── contract/ # canonical inbound wire types
│ │ ├── application/ # use-case port + implementation + mocks
│ │ ├── port/ # outbound ports + mocks
│ │ ├── adapter/
│ │ │ ├── in/http/ # echo v5 driving adapter
│ │ │ ├── in/grpc/ # gRPC driving adapter
│ │ │ └── out/postgres/ # GORM repository + models + migrations
│ │ └── di/ # feature wiring
│ ├── platform/ # cross-cutting infrastructure
│ │ ├── config/ # validated viper config
│ │ ├── db/ # gorm pool
│ │ ├── valkey/ # valkey client
│ │ ├── errors/ # typed errors + mappers
│ │ ├── middleware/ # recover, request-id, access-log, cors, otel
│ │ ├── server/ # echo + grpc bootstrap, shutdown
│ │ └── telemetry/ # zerolog, tracer, meter, health
│ └── testutil/ # shared test helpers + fixtures
├── pkg/
│ ├── api/
│ │ ├── errcodes/ # published error-code constants
│ │ └── v1/ # published inbound contract (alias facade)
│ └── uuidgen/
├── test/
│ └── e2e/ # end-to-end tests (task test-e2e)
├── .editorconfig
├── .env.example
├── .gitattributes
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── compose.yml
├── config.yaml
├── Containerfile
├── Containerfile.migrate
├── LICENSE
├── README.md
└── Taskfile.yml
The template follows clean (DDD) architecture inside each feature, with all dependencies pointing inward:
consumer services ──> pkg/api/v1 ──> features/*/contract (published contract, outward-only)
adapter/in/{http,grpc} ──> application.Service ──> port.Repository <── adapter/out/postgres
all layers ──> domain (entities + sentinel errors)
platform/* ── cross-cutting, never imports features/**
domainholds entities and sentinel errors (stdlib + uuid only).contractholds the canonical inbound wire types (json/validate tags, zero dependencies) — the single source of the API shapes.applicationdeclares the inbound use-case port (Service, speaking contract types) and itsUsecaseimplementation.portdeclares the outbound (driven) ports;adapter/out/postgressatisfies them structurally with GORM (over pgx) and owns the persistence models and SQL migrations.adapter/in/{http,grpc}are driving adapters: the echo handler binds contract types directly; the gRPC server maps protobuf ↔ contract.internal/platformconsolidates cross-cutting infrastructure: config, db pool, valkey, typed errors, middleware, servers, telemetry.
Published inbound contract. pkg/api/v1 is an alias facade over the feature's contract types plus the error codes in pkg/api/errcodes, so another Go service can construct payloads and interpret the {"error": code, "message": msg} envelope without importing server internals. Internal code never imports pkg/api/v1. gRPC consumers import api/pb/... directly.
Executable dependency gates. internal/architecture_test.go scans imports across internal/ and fails when a layer reaches sideways or outward: facade imports, domain/contract purity, application's allowlist, adapter separation, and platform's feature-agnosticism. It runs as part of task test.
Composition uses samber/do/v2: every layer exposes Register(c *do.Injector) error. internal/app is the reusable composition root that wires the DI container; cmd/server/main.go is a thin entry point that loads config, sets build-time vars (Version/CommitSHA/BuildTime), and calls app.Run, which bootstraps the container in dependency order:
platform (config → telemetry → db → valkey → server) → features
Migrations are feature-owned: each feature's SQL lives in its adapter/out/postgres/migrations/ and is embedded per feature; cmd/migrate merges every feature's migrations via migrationSources() in cmd/migrate/fsmerge.go, so deleting a feature deletes its schema with it.
Configuration is loaded from config.yaml and the environment (no prefix) into a typed, validated struct via spf13/viper and go-playground/validator.
- Remove
internal/features/example/. - Remove
api/proto/example/andapi/pb/example/. - Replace the example aliases in
pkg/api/v1/models.gowith your feature's contract types. - Remove the
examplemigrationsentry frommigrationSources()incmd/migrate/fsmerge.go. - Remove the
exampledi.Register(injector)call frominternal/app/app.go(and its import ofinternal/features/example/di). - Delete the
example:block fromconfig.yamland.env.example. - Update the
migrate-*paths inTaskfile.ymlto your feature's migrations directory.
Then add your own feature packages under internal/features/ and wire them in internal/app/app.go.
- Unit tests (hermetic, mocked):
task testorgo test -race -tags=unit ./... - Integration tests (requires postgres + valkey):
task test-integration - End-to-end tests:
task test-e2e
Containerfilebuilds a multi-stage distroless/non-root server image.Containerfile.migratebuilds a self-contained migration binary that embeds migrations viago:embed.compose.ymlruns postgres, valkey, migrate, and server locally.- Kubernetes manifests are under
deployments/kustomize/. goreleaser.ymlhandles cross-platform binary releases.