dotmd

Go Microservice

Claude Code instructions for Go microservices covering layering, context propagation, and reliability patterns.

By dotmd TeamCC0Published Feb 19, 2026View source ↗

Install path

Use this file for each supported tool in your project.

  • Claude Code: Save as CLAUDE.md in your project at CLAUDE.md.

Configuration

CLAUDE.md

1# Go Microservice
2
3HTTP service in Go. Prioritize small interfaces, explicit error handling, and production-safe defaults. Every change must pass `go vet`, `golangci-lint run`, and `go test ./...` before committing.
4
5## Quick Reference
6
7| Task | Command |
8|---|---|
9| Run service | `go run ./cmd/server` |
10| Run all tests | `go test ./... -race -count=1` |
11| Run single package tests | `go test ./internal/order/... -v` |
12| Integration tests only | `go test ./... -tags=integration -race` |
13| Lint | `golangci-lint run ./...` |
14| Vet | `go vet ./...` |
15| Build binary | `go build -o bin/server ./cmd/server` |
16| Generate (mocks, sqlc, etc.) | `go generate ./...` |
17| Tidy deps | `go mod tidy` |
18| Docker up (full stack) | `docker compose up -d` |
19| Docker rebuild | `docker compose up -d --build` |
20| Migrate up (goose) | `goose -dir migrations postgres "$DATABASE_URL" up` |
21| Migrate create (goose) | `goose -dir migrations create <name> sql` |
22| Migrate up (atlas) | `atlas migrate apply --url "$DATABASE_URL"` |
23| Migrate up (golang-migrate) | `migrate -path migrations -database "$DATABASE_URL" up` |
24
25Pick the migration tool that already exists in `go.mod`. If none, prefer goose.
26
27## Project Structure
28
29```
30├── cmd/
31│ └── server/ # main.go — wires everything, starts HTTP
32├── internal/
33│ ├── config/ # env parsing (envconfig / viper / koanf)
34│ ├── handler/ # HTTP handlers — thin, call services
35│ ├── middleware/ # auth, logging, recovery, request-id
36│ ├── service/ # business logic — no HTTP concepts
37│ ├── repository/ # data access — one file per aggregate
38│ ├── model/ # domain types, value objects
39│ └── platform/ # infra wiring: DB, cache, messaging clients
40├── migrations/ # SQL migration files (sequential numbered)
41├── pkg/ # Exported utilities (only if truly reusable)
42├── api/ # OpenAPI specs, proto files
43├── scripts/ # Dev and CI helper scripts
44├── .golangci.yml # Linter config
45├── docker-compose.yml
46├── Dockerfile
47└── Makefile
48```
49
50`internal/` is the default. Only move to `pkg/` when another service imports it. Handler → Service → Repository is the dependency direction. Never skip layers.
51
52## Code Conventions
53
54### Errors
55
56- Wrap errors with `fmt.Errorf("operation context: %w", err)` — always use `%w` for wrapping.
57- Define domain errors as sentinel values in `internal/model/errors.go`:
58 ```go
59 var (
60 ErrNotFound = errors.New("not found")
61 ErrConflict = errors.New("conflict")
62 ErrUnauthorized = errors.New("unauthorized")
63 )
64 ```
65- Handlers map domain errors to HTTP status codes. Services never import `net/http`.
66- Check with `errors.Is()` and `errors.As()`, never compare strings.
67
68### context.Context
69
70- First parameter to every exported function: `ctx context.Context`.
71- Pass context through the entire call chain: handler → service → repository → DB query.
72- Attach request-scoped values (request ID, user ID, trace ID) via middleware.
73- Never store `context.Context` in a struct.
74- Use `context.WithTimeout` for outbound calls (HTTP, DB, gRPC) — 5s default for DB, 10s for external APIs.
75
76### Logging
77
78- Use structured logging: `log/slog` (stdlib, Go 1.21+). Fall back to `zerolog` or `zap` if already in `go.mod`.
79- Always log with context: `slog.InfoContext(ctx, "order created", "order_id", id)`.
80- Log at handler entry/exit and on errors. Do not log in hot loops.
81- Levels: `Debug` for dev tracing, `Info` for business events, `Warn` for recoverable issues, `Error` for failures requiring attention.
82- Never log credentials, tokens, PII, or full request bodies.
83
84### Configuration & Environment
85
86- All config via environment variables. Parse in `internal/config/` at startup.
87- Use `envconfig`, `koanf`, or `viper` — whichever is in `go.mod`. If none, use `envconfig`.
88- Fail fast on missing required config: validate in `config.Load()`, return error, crash in `main()`.
89- No globals for config. Pass config struct (or relevant subset) via constructor injection.
90
91### Dependency Injection
92
93- Constructor injection, no framework. Wire in `cmd/server/main.go`:
94 ```go
95 repo := repository.NewOrderRepo(db)
96 svc := service.NewOrderService(repo, logger)
97 h := handler.NewOrderHandler(svc, logger)
98 ```
99- Accept interfaces, return structs.
100- Keep interfaces small — 1-3 methods. Define interfaces where they're used (in the consumer package), not where they're implemented.
101
102### HTTP Handlers
103
104- Detect the router in `go.mod` (chi / gin / echo / stdlib `net/http`) and follow its patterns.
105- Handlers decode request → call service → encode response. No business logic.
106- Always set `Content-Type: application/json` for JSON responses.
107- Return consistent error envelope:
108 ```json
109 {"error": {"code": "not_found", "message": "order 123 not found"}}
110 ```
111- Middleware order: recovery → request-id → logging → auth → rate-limit.
112
113### Database
114
115- Use `pgx` (preferred) or `database/sql` for Postgres. Use `sqlc` for type-safe SQL if present.
116- One `*pgxpool.Pool` created in `main()`, passed down.
117- Transactions live in the service layer:
118 ```go
119 func (s *OrderService) PlaceOrder(ctx context.Context, req PlaceOrderReq) error {
120 tx, err := s.pool.Begin(ctx)
121 if err != nil { return fmt.Errorf("begin tx: %w", err) }
122 defer tx.Rollback(ctx)
123 // ... repo calls using tx ...
124 return tx.Commit(ctx)
125 }
126 ```
127- Always use query parameters (`$1`, `$2`), never string concatenation.
128- Close rows: `defer rows.Close()`.
129
130### Timeouts & Graceful Shutdown
131
132- `http.Server.ReadTimeout`: 5s. `WriteTimeout`: 10s. `IdleTimeout`: 120s.
133- Graceful shutdown in `main()`:
134 ```go
135 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
136 defer stop()
137 // ... start server ...
138 <-ctx.Done()
139 shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
140 defer cancel()
141 srv.Shutdown(shutdownCtx)
142 ```
143
144## Testing Strategy
145
146### Unit Tests
147
148- Colocate with source: `order_service.go` → `order_service_test.go` in the same package.
149- Table-driven tests with `t.Run` subtests:
150 ```go
151 tests := []struct {
152 name string
153 input PlaceOrderReq
154 wantErr error
155 }{
156 {"valid order", PlaceOrderReq{...}, nil},
157 {"empty items", PlaceOrderReq{}, ErrValidation},
158 }
159 for _, tt := range tests {
160 t.Run(tt.name, func(t *testing.T) {
161 err := svc.PlaceOrder(ctx, tt.input)
162 assert.ErrorIs(t, err, tt.wantErr)
163 })
164 }
165 ```
166- Mock interfaces with hand-written mocks or `gomock`/`mockery` if already in use.
167- Use `testify/assert` and `testify/require` for assertions — `require` for fatal, `assert` for non-fatal.
168
169### Integration Tests
170
171- Guard with build tag: `//go:build integration`.
172- Use `testcontainers-go` for Postgres, Redis, etc.:
173 ```go
174 func TestOrderRepo_Integration(t *testing.T) {
175 if testing.Short() { t.Skip("skipping integration test") }
176 ctx := context.Background()
177 pg, err := postgres.Run(ctx, "postgres:16-alpine")
178 require.NoError(t, err)
179 t.Cleanup(func() { pg.Terminate(ctx) })
180 // ... run migrations, test repo methods ...
181 }
182 ```
183- Run migrations against the test container before assertions.
184- Each test gets a fresh DB or uses transactions that roll back.
185
186### Test Naming
187
188- Functions: `Test<Type>_<Method>_<scenario>` — e.g. `TestOrderService_PlaceOrder_EmptyItems`.
189- Files: `<source>_test.go`.
190
191### What to Test
192
193- Services: all business rules, edge cases, error paths.
194- Repositories: integration tests against real DB.
195- Handlers: request decoding, status codes, error mapping. Use `httptest.NewRecorder()`.
196- Skip testing: auto-generated code, simple struct definitions, `main()`.
197
198## When Using Claude Code
199
200### Exploring the Codebase
201
202Before making changes, orient yourself:
203
204```
205# Find the router setup
206rg "NewRouter\|NewMux\|gin.New\|echo.New\|chi.NewRouter" cmd/ internal/
207
208# Find where an entity is defined
209rg "type Order struct" internal/
210
211# Check existing patterns for a new handler
212cat internal/handler/order.go
213
214# See what interfaces a service depends on
215rg "type.*interface" internal/service/
216
217# Check migration history
218ls -la migrations/
219```
220
221### Making Changes
222
2231. **Read first.** Before writing code, read the relevant handler, service, and repository files. Understand the existing pattern before replicating it.
2242. **Follow existing patterns.** If the repo uses `chi`, don't introduce `gin`. If errors are wrapped with `fmt.Errorf`, don't switch to `pkg/errors`. Match what's there.
2253. **Run checks after every change:**
226 ```bash
227 go vet ./...
228 golangci-lint run ./...
229 go test ./... -race -count=1
230 ```
2314. **One concern per commit.** Don't mix a refactor with a feature. Commit the migration separately from the handler.
2325. **Generate after schema changes.** If you modify a `.sql` query file or proto, run `go generate ./...` and include generated files in the commit.
233
234### When to Ask vs. Proceed
235
236**Just do it:**
237- Adding a new handler that follows an existing pattern
238- Writing tests for existing code
239- Fixing lint errors
240- Adding error wrapping
241- Creating a migration for a new table
242
243**Ask first:**
244- Changing the router, logger, or DB driver
245- Adding a new dependency to `go.mod`
246- Modifying middleware that affects all routes
247- Changing the project structure or package layout
248- Anything that touches auth/authz logic
249
250### Git Workflow
251
252- Branch from `main`. Name: `feat/<thing>`, `fix/<thing>`, `chore/<thing>`.
253- Write a clear commit message: imperative mood, reference ticket if exists.
254- Before pushing, always run the full check suite:
255 ```bash
256 go mod tidy
257 go vet ./...
258 golangci-lint run ./...
259 go test ./... -race -count=1
260 ```
261- If tests fail, fix them before committing. Do not skip or comment out tests.
262
263### Common Pitfalls
264
265- **Forgetting `-race` flag.** Always test with `-race`. Data races in Go are silent killers.
266- **Nil pointer on zero-value structs.** Check for nil before dereferencing, especially from DB queries.
267- **Goroutine leaks.** If you spawn a goroutine, ensure it has a shutdown path via context cancellation or channel close.
268- **Import cycles.** If you hit one, you're probably putting interfaces in the wrong package. Move the interface to the consumer.
269- **Exported types in `internal/`.** This is fine — `internal/` restricts external imports, not internal cross-package use.
270

Community feedback

0 found this helpful

Works with: