# Cheat sheet — v2 Architecture

The compressed essence. Skim the matching row after each lesson.

## The v2 module skeleton (memorise this shape)
```
modules/{name}/
  domain/            entities + rules. eureka: pure Go (no pgtype).
                     spike: domain/model (pgtype) + domain/dto (plain Go)
  application/  OR  usecase/       business logic
    commands/        WRITE handlers        (eureka: usecase/, one op per file)
    queries/         READ handlers
    consumers/       Kafka handlers (shared logic)
  infrastructure/ OR repository/
    repo_port.go / storage.go   PORTS  = interfaces (domain + database.Ext only)
    repositories/ / postgres/   ADAPTERS = concrete pgx impls
  controller/ OR transport/     inbound adapters: grpc / http / kafka / nats
```

## The dependency rule (the whole point)
```
controller ──▶ handler/usecase ──▶ PORT (interface) ◀── ADAPTER (postgres)
                                       ▲
        the core owns the port; the adapter implements it;
        NOBODY in the core imports the adapter package.
```
- Port interfaces reference only **domain types + `database.Ext`** — never pgx/pgtype.
- Wiring (concrete adapter → port) happens ONLY at the **composition root**:
  `cmd/server/{svc}/gserver.go` (eureka) or the controller constructor (spike).
- **Leak to avoid:** conversationmgmt `core/service/conversation_modifier.go:6,38-45` — the core
  imports `infrastructure/postgres` and news up `&postgres.XxxRepo{}` inside itself.

## CQRS in this repo (honest version)
- **Command** = writes → `application/commands/{op}_handler.go`.
- **Query** = reads → `application/queries/{op}_handler.go`.
- **No bus.** Controller holds the handler as an *inline interface* and calls it directly.
  `spike .../controller/grpc/email_modifier_service.go:25-30` (interface), `:52-57` (wired).
- Handler holds `DB database.Ext` + repo **interfaces**; passes DB per call →
  `database.ExecInTx(ctx, cmd.DB, func(ctx, tx){ repoA.Write(tx); repoB.Write(tx) })`
  runs multiple writes atomically. (`spike CreateEmailHandler.CreateEmail:42`.)
- **Fowler's caveat (say it in interviews):** CQRS "adds risky complexity" — use it only where the
  read and write models genuinely diverge.

## The read/write asymmetry (eureka rule)
| Op | How | Why |
|---|---|---|
| **Read** | sqlc `Querier` **directly** from the usecase, any table | reads have no invariants to protect |
| **Write** | **only** through a repository **interface** | invariants + events + observability never bypassed |
> "Never call sqlc mutating queries directly from a usecase." — `internal/eureka/CLAUDE.md`
> Live: `assessment/usecase/reset_student_study_plan.go:25` (read) vs `:61-77` (writes in ExecInTxV2).

## sqlc flow (eureka only)
```
1. write SQL   v2/modules/{m}/repository/postgres/queries/{table}.sql
     -- name: BulkRemoveX :exec
     UPDATE x SET deleted_at = NOW() WHERE session_id = ANY(@session_ids::text[]);
2. generate    make eureka-gen-sqlc-v2      # WASM plugin → db/ + mock/
3. artifacts   .../postgres/db/{db.go, querier.go, models.go, {table}.sql.go}
4. wrap it     type XRepo struct{ querier x_db.Querier }
               func NewXRepo() *XRepo { return &XRepo{querier: x_db.New()} }   # constructor!
               func (r *XRepo) BulkRemoveX(ctx, ext database.Ext, ids []string) error {
                   return r.querier.BulkRemoveX(ctx, ext, x_db.BulkRemoveXParams{...}) }
```
- `emit_interface` → mockable `Querier`; `go_generate_mock` → mock auto-built.
- `emit_methods_with_db_argument` → `New()` takes no DB; DB passed per method (tx injection).
- `emit_dynamic_filter` → optional WHERE clauses; `emit_tracing` → a span per query.
- `emit_err_nil_if_no_rows` → `(nil, nil)` instead of `pgx.ErrNoRows`.
- **Constructor, not `&Repo{}`** — the querier field would be nil → panic. (Stateless spike repos
  are exempt: no querier field.)
- **Hybrid reality:** `learning_time` uses the querier in only 1 method — `assessment` is the fullest.

## Error translation across layers (verified convention)
```
Repository:  pgx.ErrNoRows           → common.ErrNoRowsExisted
Usecase:     common.ErrNoRowsExisted → domain.ErrXNotFound
Transport:   domain.ErrXNotFound     → status.Error(codes.NotFound, ...)
```

## The module-quality ladder (what to copy / avoid)
- ✅ **Copy:** notification `system_notification` (canonical CQRS), spike `email`.
- ⚠️ **Careful:** notification `announcement` (leaks `pgtype` into usecase), conversationmgmt
  `conversation` (leaks DI: core→infra).
- ❌ **Avoid:** notification `tagmgmt` (legacy), any v1 `entities/repositories/services`.

## Interview one-liners
- **Hexagonal?** "Ports & adapters. The core defines interfaces (ports); infra provides
  implementations (adapters); the core never imports infra, so I can swap the DB or test the core
  with a mock. We do it per module — `core/port` + `infrastructure`."
- **CQRS?** "Separate write model from read model. We split `commands/` and `queries/` handlers —
  no bus, controllers call them directly. Fowler's right that it adds complexity, so it's per-module,
  not global."
- **sqlc?** "Write SQL, generate type-safe Go — compile-time column/param checking, a mockable
  `Querier`. In eureka we read via the querier directly but always write through a repo interface,
  so invariants and events aren't bypassed."
- **Keeping the DB out of the domain?** "The port interface takes only domain types and
  `database.Ext`, never pgx. Driver types stay in the adapter. When `pgtype` shows up in a usecase
  signature, that's a leak."
