# Repo Go Map — ground truth

How Go is actually written in **our** codebase, focused on conversationmgmt,
notification (`internal/notification`), spike (`internal/spike`), and the shared
`internal/golibs`. This is the factual backbone every lesson is anchored to. All paths
are repo-relative; `file:line` refs point at the code on this branch.

> Verified by a code scan on 2026-07-11. Repo Go version: **go 1.25.8** (`go.mod`).
> If a lesson and this file disagree, trust the code — re-verify and fix here.
> The repo also documents its own conventions in `.claude/rules/go-code-style.md`,
> `.claude/rules/go-test-style.md`, and each service's `CLAUDE.md`.

---

## Project structure

Two coexisting layouts:

- **spike & notification `modules/`** — CQRS + hexagonal (ports & adapters):
  `domain/{model,dto}` · `application/{commands,queries,consumers}` (one handler +
  payload per file) · `infrastructure/repo_port.go` (interfaces) +
  `infrastructure/repositories/` (pgx adapters) · `controller/{grpc,http,kafka}` (thin).
- **conversationmgmt** — `core/{domain,port,service}` + `infrastructure/` +
  `controller/`. Cleanest split: `core/domain` entities are **plain Go**
  (`core/domain/conversation.go:9`); pgtype lives only in
  `infrastructure/postgres/dto/` (`ConversationPgDTO`, dto/conversation.go:15).
- **notification** also has a **legacy flat layer** (`services/`, `repositories/`,
  `entities/`, `transports/`, `subscribers/`). Per `internal/notification/CLAUDE.md`,
  `system_notification` is the canonical CQRS template; `tagmgmt` is legacy-don't-copy.

**Import ordering** — three blank-line-separated groups: (1) stdlib, (2)
`github.com/manabie-com/backend/...`, (3) third-party. E.g.
`internal/golibs/kafka/consumer.go:3-17`.

---

## Value semantics & receivers

- **Everything is a value (copied on assignment/pass).** Pointers are explicit.
- **Receivers**: overwhelmingly pointer (`func (repo *EmailRepo) ...`). Value-receiver
  examples: `func (f future) Await()` (`internal/golibs/asyncawait/async.go:13`),
  `func (b bootstrapper[T]) shutdownServer(...)` (`bootstrap/bootstrap.go:267`). Blank
  receiver when unused: `func (*Email) TableName() string` (`spike/.../model/email.go`).
- **Documented rule** (`.claude/rules/go-code-style.md`): use a pointer receiver only
  for in-place mutation, large structs, `(nil, err)` disambiguation, or consistency;
  **never** pointer-to-slice/map/interface; prefer `[]Book` over `[]*Book`.
- **Stateless adapters as zero-value structs**: `type EmailRepo struct{}`
  (`spike/.../infrastructure/repositories/email.go:17`) — usable with no init.

## Structs / entities

pgtype DB entities with `TableName()` / `FieldMap()` / slice `Add()`
(`internal/spike/modules/email/domain/model/email.go:9-70`):
```go
type Email struct {
    EmailID   pgtype.Text
    Content   pgtype.JSONB
    CreatedAt pgtype.Timestamptz
}
func (*Email) TableName() string { return "emails" }
func (e *Email) FieldMap() (fields []string, values []interface{}) { ... } // named returns
type Emails []*Email
func (u *Emails) Add() database.Entity { e := &Email{}; *u = append(*u, e); return e }
```
NULL via `field.Set(nil)` (`email.go:23`). JSON tags dominate; SQL columns come from
`FieldMap()`, not tags. `json:"-"` hides fields (async_notification_upserting.go:29).

## Slices & maps

- `type Emails []*Email` + `Add()` appends — the slice-scan pattern.
- **Map iteration is nondeterministic**: dynamic UPDATE builds `col = $N` by ranging a
  `map[string]interface{}` (`email.go:138-145`) — correct only because values are
  appended in the *same* loop (paired append, not order).
- Generic slice helpers `internal/golibs/sliceutils/`: `Map`, `Filter`, `Reduce`,
  `Contains[T comparable]`, `Chunk`. Used e.g. `sliceutils.RemoveDuplicates(mediaIDs)`.

## Interfaces & DI

- **Interface-at-consumer** (dominant in controllers): inline anonymous interface so it
  mocks cleanly — `spike/.../controller/grpc/email_modifier_service.go:25-30`.
- **Segregated port files** at hex seams: `spike/.../infrastructure/repo_port.go:11`,
  `conversationmgmt/.../core/port/repository/conversation.go:10`.
- **Constructors** wire concrete deps into interface fields at the composition root:
  `NewEmailModifierService` (`email_modifier_service.go:39-67`).
- **Two db-injection styles**: spike stores `DB database.Ext` on the struct;
  notification's canonical `system_notification` passes `db database.QueryExecer`
  **per method** so one tx spans handlers (`upsert_system_notification_command_handler.go:17`).

## Generics

- Bootstrap servicer family, all `[T any]`: `KafkaServicer[T]` (`bootstrap/kafka.go:16`),
  `AllServicer[T]`/`BaseServicer[T]`/`GRPCServicer[T]` (`bootstrap/grpc.go:35-68`),
  `bootstrapper[T]` (`bootstrap.go:31`). Feature detection: `interface{}(s).(KafkaServicer[T])`.
- `extract[T any](config, field) (*T, error)` via reflection (`config.go:30`);
  `RegisterJob[T any](name, JobFunc[T])` (`command.go:240`).
- `sliceutils.Map[T, T2 any]`, `Contains[T comparable]`, etc.

## Errors

- **Wrap with `%w`**: `fmt.Errorf("call.Site: %w", err)` (`repositories/email.go:29,109`).
  ⚠️ **Inconsistency**: some handlers use `%v` (`create_email_handler.go:34,45`), which
  breaks `errors.Is/As` chains — teach as a real trap, not a universal rule.
- **Sentinels** at package scope: `spike/.../grpc/validator.go:22-33` (`ErrEmpty`,
  `ErrMediasSizeExceed`, …); `internal/golibs/kafka/kafka.go:26`.
- **`errors.Is`/`As`** for classification: `consumer.go:178` (`io.EOF`),
  `validator.go:278-281` (switch on sentinel to pick a proto error code).
- **gRPC status mapping only at the controller boundary** —
  `spike/.../grpc/email_modifier_send_email.go:20-38`:
  `InvalidArgument` (validation) · `Internal` (infra) · `ResourceExhausted` (limits) ·
  `NotFound` · `PermissionDenied` (all auth failures). Shared PG→status:
  `errorx.ToStatusError` (`internal/golibs/errorx/errorx.go:56-78`). Aggregation via
  `go.uber.org/multierr` and `github.com/pkg/errors` (`errors.Wrap`).

## Concurrency (richest: the Kafka engine, `internal/golibs/kafka/`)

- Goroutine per consumer, tracked by `sync.WaitGroup`, guarded by `sync.Mutex`, with
  `defer` cleanup (`kafka.go:587-606`); args passed as params (no loop-var capture).
- Worker loops `select` on `ctx.Done()` (`runConsumerWithoutSelfEnd`, `kafka.go:672`);
  exponential backoff `consumerBackoff` (`kafka.go:76`); dial jitter (`kafka.go:291`).
- Health-check goroutine: `time.NewTicker` + stop channel + WaitGroup (`kafka.go:337`).
- Graceful shutdown `Close()` (`kafka.go:788`): stop health, cancel each consumer ctx,
  `Wait()`.
- **Per-consumer cancellation** `context.WithCancel` (`consumer.go:38`); per-attempt
  `context.WithTimeout(context.Background(), retryTimeout)` (`consumer.go:112`).
- `errgroup` only in `bootstrap/bootstrap.go:321-359` (HTTP+gRPC serve loop, with
  `signal.NotifyContext(SIGTERM,SIGINT)`). Services use raw `sync.WaitGroup` + buffered
  error channel + `multierr`
  (`conversationmgmt/.../support_conversation_reader_staff_get_conversations.go:48-93`).
- Worker pool with `sync.Once` + `recover()`
  (`conversationmgmt/.../http/util/worker_pool.go`); TaskRunner respawns panicked
  workers (`conversationmgmt/pkg/golibs/taskrunner/task_runner.go`); promise/future
  (`internal/golibs/asyncawait/async.go`). Retry helper `internal/golibs/try/`.

## Functional options & idioms

- **Functional options** (canonical): `internal/golibs/kafka/consumer_option.go` —
  option is an interface with a func adapter; constructors `AlwaysCommit()` (`:183`),
  `Instances(n)` (`:203`, validates & returns error). Defaults slice + user opts.
- **Builder/chaining**: `RegisterOpt[T]` `.WithGRPC().WithKafkaServicer()` (`command.go`).
- **Named returns**: `FieldMap() (fields, values)`; `(isContinueToNextMsg bool, err error)`.
- **`defer`**: `defer span.End()` after every `interceptors.StartSpan`; `defer cancel()`;
  `defer result.Close()`; `defer recover()` in worker pools.
- **Closures**: tx callback `database.ExecInTx(ctx, db, func(ctx, tx) error {...})`.

## Context & request-scoped data

- Context keys are **private int types** (`interceptors/auth.go:346-351`) to avoid
  collisions. Getters/setters: `UserIDFromContext`, `ContextWithJWTClaims`,
  `ResourcePathFromContext` (`auth.go:353-477`). Security rule: `resource_path` always
  from context, never the request.
- **Kafka propagation**: claims serialized into headers on publish, rehydrated on
  consume — `TraceCarrierAndClaimInfoFromMessageHeaders` + `ContextWithJWTClaims`
  (`consumer.go:114-131`); header names `Kafka-User-ID`/`Kafka-Resource-Path`.

## Testing (`.claude/rules/go-test-style.md`)

`t.Parallel()` · `context.WithTimeout` · testify `assert`/`mock` · `mock_database.Ext/Tx`
· generated repo mocks · `t.Run` subtests · `// arrange // act // assert` ·
`mock.AssertExpectationsForObjects`. Representative:
`internal/spike/modules/email/application/commands/create_email_handler_test.go`
(tx expectations `mockDB.On("Begin", ctx).Return(mockTx, nil)`; argument assertions via
`mock.MatchedBy(...)`). **Custom mock tool** (`cmd/utils/mock/genSpike.go`) generates
`MockEmailRepo{ mock.Mock }` (`mock/spike/.../email_repo.go:13`); regenerate with
`make service=spike gen-mock-service`.

## Bootstrap (composition root)

A service implements `BaseServicer[Config]` (+ optional `GRPCServicer`/`KafkaServicer`/…),
registered via `bootstrap.WithGRPC(s).WithKafkaServicer(s).Register(s)` (`command.go`).
Lifecycle (`bootstrap.go`): load yaml (`configs.LoadAllContext[T]`) → `initDeps`
(logger, DB, NATS, Kafka, Unleash) → `initServer` (type-switch servicer detection) →
`serve` (errgroup + graceful shutdown). `Resources` is the DI container:
`rsc.DBWith("bob")`, `rsc.Kafka()`, `rsc.Logger()`.

## Stdlib & golibs usage (frequency across the three services)

Stdlib: `context` 608 · `fmt` 482 · `time` 290 · `strings` 112 · `encoding/json` 75 ·
`errors` 51 · `sync` 12 (rare — concurrency lives in golibs). Golibs: `database` 428
(pgx: `Ext`/`QueryExecer`/`Tx`, `ExecInTx`) · `interceptors` 236 (auth/claims/spans) ·
`idutil` 83 (`ULIDNow()`) · `kafka` 39 · `sliceutils` 32 · `unleashclient` 31 · plus
`logger` (zap), `tracer`, `errorx` (PG→status).

## Gotchas worth teaching

- Custom `MarshalJSON` via the `type Alias T` trick to avoid recursion
  (`async_notification_upserting.go:64-96`).
- Loop-var capture consciously avoided by passing as goroutine params (`kafka.go:594`).
  (Go 1.22+ fixed the classic bug; repo is 1.25 — teach both the history and today.)
- `defer` inside a `for` in a long-lived goroutine accumulates (`task_runner.go:161`).
- Goroutine-leak guards: buffered error channel sized to producers + closer goroutine;
  `select` on `ctx.Done()` in every loop; `sync.Once` for idempotent channel close.
- `context.Background()` used deliberately for the handler timeout so an upstream cancel
  doesn't kill retries (`consumer.go:112`).
