Go Library Builder
Scaffolds a new reusable Go library module in the ralvarezdev house style, distilled from the identity, ratelimit, and email libraries. This skill is the composition root for three others — it does not restate them:
- go-architect — every Go idiom (memory alignment, errors, iterators, linters,
//go:embed SQL). This skill assumes it.
- hexagonal-arch — ports & adapters, dependency direction. This skill applies it at library scale.
- ddd-architect — entities, value objects, bounded contexts. This skill uses it for domain shape.
Also leans on repo-tooling-architect (mise/Task/editorconfig/renovate) and sql-architect (persistence adapters). File skeletons and full tooling templates live in RECIPES.md and assets/. Pinned versions in STACK.md.
1. What it produces
A standalone Go module publishable as <namespace>/<name> where hexagonal layers map onto Go package boundaries, not layer-named folders. There is never a domain/, application/, ports/, or adapters/ directory — the package is the layer.
<namespace> is a parameter — ask for it at bootstrap (§13.1); default github.com/ralvarezdev. It sets the module prefix, every intra-repo import path, goimports -local, and sqlc type-override package paths. Nothing else in the house style changes with it.
Two scales, same rules (§9 decides which):
- Flat single-port library (
ratelimit, email) — root package = domain + one port; one subpackage per adapter.
- Vertical-slice library (
identity) — one package per bounded context, each internally hexagonal, plus an app/ application layer and internal/acl/ seams.
2. Module & naming convention
- Module path:
<namespace>/<name> (default namespace github.com/ralvarezdev) — <name> is the domain noun only. No go- prefix, no methodology prefix (ddd-, hex-).
- Packages: short, lowercase, single word. Root/slice package = the domain concept (
email, user, auth). Adapter package = the technology (smtp, valkey, postgres, jwt), never the pattern (repository, infra).
- The adapter file is named for the port it implements, identical across adapters (
mailer.go, limiter.go, store.go). The package disambiguates, so the type is smtp.Mailer, not SmtpMailer — kill the stutter.
- Concrete type =
Mailer / Store / Service / Issuer; constructor = New (or NewMailer when the package holds several). Docs dated docs/YYYY-MM-DD-<topic>.md.
3. Domain purity — the load-bearing invariant
The root/domain package imports stdlib + google/uuid only. Zero third-party. This is the invariant everything else protects: only adapters may import a driver, an HTTP client, or a crypto library. Static check: grep the domain package's imports — a single third-party line is a bug. See hexagonal-arch §2.
4. Ports — narrow, composed, domain-owned
Interfaces are declared by the domain, where consumed, per go-architect §4. House specifics:
- Cap at ~3 methods (
interfacebloat). Grow by composition, not method count: UserStore = UserRecordReader + UserRecordWriter + UserRecordQuery. Callers depend on the narrowest sub-interface they use.
context.Context first, plain domain data across the seam. No driver types in a port signature — that's a leaky port (hexagonal-arch §6).
- Name for role, not tech:
TokenIssuer, AttemptLimiter, UserLookup. Suffix …Port only when …er/…Store would collide.
- One instance may satisfy many ports — a single
*postgres.Store can implement eight interfaces; wiring passes the same pointer.
5. Constructors & configuration
- Accept interfaces, return concrete
*T (ireturn). Callers hold interfaces; wiring holds structs.
- Constructor injection only. No globals, no
init() (gochecknoglobals + gochecknoinits). Dependencies are constructor parameters.
- Config is a plain validated struct, not functional options. The house style deliberately rejects
Option func(*T). Fill zero values with a setDefaults() method over Default* constants; validate in New via Config.Validate() error. Rationale + the one exception in RECIPES §2.
- The library never reads env, files, or viper. The consuming app owns all config and passes secrets/DSNs/TTLs as plain values (
jwt.New(secret, accessTTL, refreshTTL)).
6. Errors
Follows go-architect §8 with three house rules:
- Sentinels are package-prefixed:
var ErrNotFound = errors.New("user: not found"). Parametrized cases get a constructor func (ErrPasswordTooShort(min int) error). errname enforces the Err… form.
- Validation aggregates with
errors.Join; every returned error is wrapped fmt.Errorf("<pkg>: <op>: %w", err) — a breadcrumb chain the caller reads with errors.Is/errors.As.
- Adapters translate infra errors to domain sentinels at the boundary (
pgx.ErrNoRows → user.ErrNotFound, unique-violation → ErrEmailTaken). A driver error must never escape an adapter. A typed error (SendError{Permanent bool}) may live in an adapter when the caller needs to branch (retry classification), but the domain only knows sentinels.
- Business "no" is not an error.
Result{Allowed: false} with nil error is a valid outcome; a non-nil error means the mechanism itself failed.
7. Domain shape — entities & value objects
Per ddd-architect §3:
- Value objects self-validate at construction, unexported fields, zero value invalid, built via
New… (NewEmail, NewAddress). Reach for them aggressively — a validated Email beats a bare string everywhere.
- Entities carry behavior, not just data —
User.Activate(now), with private transition maps enforcing legal state changes. No anemic bags of getters.
- No generics. Reuse comes from interface composition, not type parameters. Add a generic only when a data-structure genuinely needs it (rare in these libraries).
8. Adapters — one per technology, N per port
The reuse axis is multiple adapters behind one port, swapped at wiring time:
- Production — the real backing tech (
smtp, postgres, valkey).
- Dev/observability — e.g. a
logger adapter that logs instead of sending.
- Test — a real, hand-written in-memory adapter (
memory) with Sent()/Reset() helpers. This is the test double — see §10.
Each adapter imports the domain, translates errors inward (§6), and keeps its third-party dep contained. A port with only ever one adapter is ceremony — inline it until a second (the in-memory test double counts) justifies the seam (hexagonal-arch §7).
9. Scaling — start flat, grow to slices
Default to the flat shape. Split into vertical slices only when a genuine second bounded context appears (distinct ubiquitous language, per ddd-architect §1). When you do:
- One package per bounded context (
user/, auth/), each internally hexagonal (domain + ports + service + adapter subpackages).
app/ is the application layer — cross-context use-case orchestration (Register → policy → hash → create → issue OTT → email). No business rules here; it sequences ports. Mirrors go-architect §16's app vs cmd split.
- A context never imports another's domain. It declares its own port (
UserLookup) and an internal/acl/ adapter translates the neighbor's service and errors across the seam — the anticorruption layer of ddd-architect §8.
10. Testing — real doubles, no mock framework
Per go-architect §9, sharpened:
- No mock library, no generated mocks. Unit isolation comes from the narrow ports — a fake is a plain struct, and the in-memory adapter (§8) is the reusable one.
- Integration via
testcontainers-go — one container per package started in TestMain (main_test.go); each test gets isolation via a rolled-back transaction (BeginTx + t.Cleanup) or per-test uuid keys. Table/keyspace named <name>_test.
t.Parallel() at parent and subtest, table-driven, black-box package x_test for the public API plus white-box package x (*_internal_test.go) only to reach unexported helpers. Gate container tests so go test -short ./... stays fast.
11. Persistence & codegen (when a DB adapter exists)
- Small/static queries — raw SQL in
.sql files via //go:embed + sqlx, per go-architect §12.
- Larger typed access —
sqlc (pgx/v5) with hand-written queries/*.sql; DB enums overridden to domain types in sqlc.yaml so generated code maps to your VOs. Schema is derived from migrations by a cmd/gen step, never hand-maintained.
- Migrations —
goose SQL with //go:embed, run through a public migrate.Up/Down/Status API and a library-owned version table (<name>_schema_version) so the lib's history is independent of the host app. See sql-architect.
12. Tooling (every repo gets the same set)
Copy from assets/; details in repo-tooling-architect and go-architect §14:
mise.toml pins the toolchain (go, task, golangci-lint, goimports, betteralign, trivy, and sqlc/goose when used).
Taskfile.yml — build, test (-race -count=1), test:short, fmt (goimports -local), lint, lint:fix, lint:doccheck, modernize (go fix), align:check/fix (betteralign), scan (trivy), tag (semver-validated), and check = the pre-commit gate.
.golangci.yml (v2) — copy go-architect's assets/golangci.yml, set goimports.local-prefixes to the module path, scope exhaustruct to the Config structs. Ship the separate .golangci.doccheck.yml for on-demand doc coverage.
.editorconfig, .gitignore, renovate.json, and a spec-first docs/YYYY-MM-DD-spec.md written so an agent can rebuild the module from it alone (list the invariants explicitly).
.rsk/ — pin the architecture skills this repo was built with (go-architect, hexagonal-arch, ddd-architect, plus sql-architect/repo-tooling-architect as used) so the conventions travel with the repo.
13. Bootstrap procedure
- Ask for
<name>, <namespace> (default github.com/ralvarezdev), and scale (§9) plus the port(s) the library exposes.
git init; write go.mod (module <namespace>/<name>, go 1.26).
- Copy the assets/ tooling set;
mise install; set Taskfile.yml's MODULE var and goimports local-prefixes to <namespace>/<name>.
- Write the domain: value objects, entities, sentinel errors, the port interface(s) — stdlib + uuid only (§3–§7).
- Write the in-memory adapter first (it doubles as the test double), then the production adapter(s) (§8).
- Add persistence/codegen only if a DB adapter exists (§11).
- Write the spec-first doc and the README (Packages table → Quick start → Testing).
task check must pass green before the first task tag VERSION=0.1.0.
14. Out of scope
- Deployable services /
cmd/ binaries — this scaffolds libraries consumers wire into their own composition root. Service layout → go-architect §16.
- Functional-options APIs, ORMs, viper-in-library — deliberately rejected by the house style (§5, §11).
- The Go idioms and linter catalog themselves — owned by go-architect.
1---2name: go-library-builder3description: Scaffolds reusable Go 1.26 library modules in the ralvarezdev DDD + hexagonal house style — domain-pure root package, ports as narrow interfaces, technology-named adapter subpackages (prod/dev/test), config structs over options, testcontainers over mocks, mise + Task + golangci tooling. Module namespace is a parameter (default github.com/ralvarezdev). Use when bootstrapping a new Go library, adding an adapter, or auditing one against the house style.4---56# Go Library Builder78Scaffolds a new **reusable Go library module** in the ralvarezdev house style, distilled from the `identity`, `ratelimit`, and `email` libraries. This skill is the *composition root* for three others — it does not restate them:910- **[go-architect](../../languages/go-architect/SKILL.md)** — every Go idiom (memory alignment, errors, iterators, linters, `//go:embed` SQL). This skill assumes it.11- **[hexagonal-arch](../../design/hexagonal-arch/SKILL.md)** — ports & adapters, dependency direction. This skill applies it *at library scale*.12- **[ddd-architect](../../design/ddd-architect/SKILL.md)** — entities, value objects, bounded contexts. This skill uses it for domain shape.1314Also leans on [repo-tooling-architect](../../tooling/repo-tooling-architect/SKILL.md) (mise/Task/editorconfig/renovate) and [sql-architect](../../databases/sql-architect/SKILL.md) (persistence adapters). File skeletons and full tooling templates live in [RECIPES.md](RECIPES.md) and [assets/](assets). Pinned versions in [STACK.md](STACK.md).1516## 1. What it produces1718A standalone Go module publishable as `<namespace>/<name>` where **hexagonal layers map onto Go package boundaries**, not layer-named folders. There is never a `domain/`, `application/`, `ports/`, or `adapters/` directory — the package *is* the layer.1920**`<namespace>` is a parameter** — ask for it at bootstrap (§13.1); default `github.com/ralvarezdev`. It sets the module prefix, every intra-repo import path, `goimports -local`, and `sqlc` type-override package paths. Nothing else in the house style changes with it.2122Two scales, same rules (§9 decides which):2324- **Flat single-port library** (`ratelimit`, `email`) — root package = domain + one port; one subpackage per adapter.25- **Vertical-slice library** (`identity`) — one package per bounded context, each internally hexagonal, plus an `app/` application layer and `internal/acl/` seams.2627## 2. Module & naming convention2829- **Module path:** `<namespace>/<name>` (default namespace `github.com/ralvarezdev`) — `<name>` is the domain noun only. No `go-` prefix, no methodology prefix (`ddd-`, `hex-`).30- **Packages:** short, lowercase, single word. Root/slice package = the domain concept (`email`, `user`, `auth`). Adapter package = the **technology** (`smtp`, `valkey`, `postgres`, `jwt`), never the pattern (`repository`, `infra`).31- **The adapter file is named for the port it implements**, identical across adapters (`mailer.go`, `limiter.go`, `store.go`). The package disambiguates, so the type is `smtp.Mailer`, not `SmtpMailer` — kill the stutter.32- **Concrete type** = `Mailer` / `Store` / `Service` / `Issuer`; **constructor** = `New` (or `NewMailer` when the package holds several). Docs dated `docs/YYYY-MM-DD-<topic>.md`.3334## 3. Domain purity — the load-bearing invariant3536The root/domain package imports **stdlib + `google/uuid` only**. Zero third-party. This is the invariant everything else protects: only adapters may import a driver, an HTTP client, or a crypto library. Static check: grep the domain package's imports — a single third-party line is a bug. See [hexagonal-arch §2](../../design/hexagonal-arch/SKILL.md#2-dependency-direction--always-inward).3738## 4. Ports — narrow, composed, domain-owned3940Interfaces are declared **by the domain, where consumed**, per [go-architect §4](../../languages/go-architect/SKILL.md#4-interfaces). House specifics:4142- **Cap at ~3 methods** (`interfacebloat`). Grow by **composition, not method count**: `UserStore = UserRecordReader + UserRecordWriter + UserRecordQuery`. Callers depend on the narrowest sub-interface they use.43- **`context.Context` first, plain domain data across the seam.** No driver types in a port signature — that's a leaky port ([hexagonal-arch §6](../../design/hexagonal-arch/SKILL.md#6-common-mistakes)).44- **Name for role, not tech:** `TokenIssuer`, `AttemptLimiter`, `UserLookup`. Suffix `…Port` only when `…er`/`…Store` would collide.45- **One instance may satisfy many ports** — a single `*postgres.Store` can implement eight interfaces; wiring passes the same pointer.4647## 5. Constructors & configuration4849- **Accept interfaces, return concrete `*T`** (`ireturn`). Callers hold interfaces; wiring holds structs.50- **Constructor injection only.** No globals, no `init()` (`gochecknoglobals` + `gochecknoinits`). Dependencies are constructor parameters.51- **Config is a plain validated struct, not functional options.** The house style deliberately rejects `Option func(*T)`. Fill zero values with a `setDefaults()` method over `Default*` constants; validate in `New` via `Config.Validate() error`. Rationale + the one exception in [RECIPES §2](RECIPES.md#2-config--constructors).52- **The library never reads env, files, or viper.** The consuming app owns all config and passes secrets/DSNs/TTLs as plain values (`jwt.New(secret, accessTTL, refreshTTL)`).5354## 6. Errors5556Follows [go-architect §8](../../languages/go-architect/SKILL.md#8-errors--panics) with three house rules:5758- **Sentinels are package-prefixed:** `var ErrNotFound = errors.New("user: not found")`. Parametrized cases get a constructor func (`ErrPasswordTooShort(min int) error`). `errname` enforces the `Err…` form.59- **Validation aggregates with `errors.Join`**; every returned error is wrapped `fmt.Errorf("<pkg>: <op>: %w", err)` — a breadcrumb chain the caller reads with `errors.Is`/`errors.As`.60- **Adapters translate infra errors to domain sentinels at the boundary** (`pgx.ErrNoRows → user.ErrNotFound`, unique-violation → `ErrEmailTaken`). A driver error must never escape an adapter. A typed error (`SendError{Permanent bool}`) may live *in* an adapter when the caller needs to branch (retry classification), but the domain only knows sentinels.61- **Business "no" is not an error.** `Result{Allowed: false}` with `nil` error is a valid outcome; a non-nil error means the mechanism itself failed.6263## 7. Domain shape — entities & value objects6465Per [ddd-architect §3](../../design/ddd-architect/SKILL.md#3-tactical-ddd--building-blocks):6667- **Value objects self-validate at construction**, unexported fields, zero value invalid, built via `New…` (`NewEmail`, `NewAddress`). Reach for them aggressively — a validated `Email` beats a bare `string` everywhere.68- **Entities carry behavior**, not just data — `User.Activate(now)`, with private transition maps enforcing legal state changes. No anemic bags of getters.69- **No generics.** Reuse comes from interface composition, not type parameters. Add a generic only when a data-structure genuinely needs it (rare in these libraries).7071## 8. Adapters — one per technology, N per port7273The reuse axis is **multiple adapters behind one port**, swapped at wiring time:7475- **Production** — the real backing tech (`smtp`, `postgres`, `valkey`).76- **Dev/observability** — e.g. a `logger` adapter that logs instead of sending.77- **Test** — a real, hand-written **in-memory** adapter (`memory`) with `Sent()`/`Reset()` helpers. This *is* the test double — see §10.7879Each adapter imports the domain, translates errors inward (§6), and keeps its third-party dep contained. A port with only ever **one** adapter is ceremony — inline it until a second (the in-memory test double counts) justifies the seam ([hexagonal-arch §7](../../design/hexagonal-arch/SKILL.md#7-when-not-to-use-it)).8081## 9. Scaling — start flat, grow to slices8283Default to the **flat** shape. Split into vertical slices only when a genuine second bounded context appears (distinct ubiquitous language, per [ddd-architect §1](../../design/ddd-architect/SKILL.md#1-strategic-ddd--bounded-contexts)). When you do:8485- **One package per bounded context** (`user/`, `auth/`), each internally hexagonal (domain + ports + service + adapter subpackages).86- **`app/`** is the application layer — cross-context use-case orchestration (`Register` → policy → hash → create → issue OTT → email). No business rules here; it sequences ports. Mirrors [go-architect §16](../../languages/go-architect/SKILL.md#16-multi-module-workspaces-gowork)'s `app` vs `cmd` split.87- **A context never imports another's domain.** It declares its own port (`UserLookup`) and an **`internal/acl/`** adapter translates the neighbor's service and errors across the seam — the anticorruption layer of [ddd-architect §8](../../design/ddd-architect/SKILL.md#8-anticorruption-layers-acl).8889## 10. Testing — real doubles, no mock framework9091Per [go-architect §9](../../languages/go-architect/SKILL.md#9-testing), sharpened:9293- **No mock library, no generated mocks.** Unit isolation comes from the narrow ports — a fake is a plain struct, and the in-memory adapter (§8) is the reusable one.94- **Integration via `testcontainers-go`** — one container per package started in `TestMain` (`main_test.go`); each test gets isolation via a rolled-back transaction (`BeginTx` + `t.Cleanup`) or per-test uuid keys. Table/keyspace named `<name>_test`.95- **`t.Parallel()` at parent and subtest**, table-driven, black-box `package x_test` for the public API plus white-box `package x` (`*_internal_test.go`) only to reach unexported helpers. Gate container tests so `go test -short ./...` stays fast.9697## 11. Persistence & codegen (when a DB adapter exists)9899- **Small/static queries** — raw SQL in `.sql` files via `//go:embed` + `sqlx`, per [go-architect §12](../../languages/go-architect/SKILL.md#12-database-access--sql-files--goembed).100- **Larger typed access** — `sqlc` (pgx/v5) with hand-written `queries/*.sql`; DB enums overridden to domain types in `sqlc.yaml` so generated code maps to your VOs. Schema is derived from migrations by a `cmd/gen` step, never hand-maintained.101- **Migrations** — `goose` SQL with `//go:embed`, run through a public `migrate.Up/Down/Status` API and a **library-owned version table** (`<name>_schema_version`) so the lib's history is independent of the host app. See [sql-architect](../../databases/sql-architect/SKILL.md).102103## 12. Tooling (every repo gets the same set)104105Copy from [assets/](assets); details in [repo-tooling-architect](../../tooling/repo-tooling-architect/SKILL.md) and [go-architect §14](../../languages/go-architect/SKILL.md#14-tooling):106107- **`mise.toml`** pins the toolchain (go, task, golangci-lint, goimports, betteralign, trivy, and sqlc/goose when used).108- **`Taskfile.yml`** — `build`, `test` (`-race -count=1`), `test:short`, `fmt` (goimports `-local`), `lint`, `lint:fix`, `lint:doccheck`, `modernize` (`go fix`), `align:check/fix` (betteralign), `scan` (trivy), `tag` (semver-validated), and `check` = the pre-commit gate.109- **`.golangci.yml`** (v2) — copy [go-architect's `assets/golangci.yml`](../../languages/go-architect/assets/golangci.yml), set `goimports.local-prefixes` to the module path, scope `exhaustruct` to the `Config` structs. Ship the separate **`.golangci.doccheck.yml`** for on-demand doc coverage.110- **`.editorconfig`**, **`.gitignore`**, **`renovate.json`**, and a **spec-first `docs/YYYY-MM-DD-spec.md`** written so an agent can rebuild the module from it alone (list the invariants explicitly).111- **`.rsk/`** — pin the architecture skills this repo was built with (`go-architect`, `hexagonal-arch`, `ddd-architect`, plus `sql-architect`/`repo-tooling-architect` as used) so the conventions travel with the repo.112113## 13. Bootstrap procedure1141151. Ask for `<name>`, `<namespace>` (default `github.com/ralvarezdev`), and scale (§9) plus the port(s) the library exposes.1162. `git init`; write `go.mod` (`module <namespace>/<name>`, `go 1.26`).1173. Copy the [assets/](assets) tooling set; `mise install`; set `Taskfile.yml`'s `MODULE` var and `goimports` `local-prefixes` to `<namespace>/<name>`.1184. Write the domain: value objects, entities, sentinel errors, the port interface(s) — stdlib + uuid only (§3–§7).1195. Write the in-memory adapter first (it doubles as the test double), then the production adapter(s) (§8).1206. Add persistence/codegen only if a DB adapter exists (§11).1217. Write the spec-first doc and the README (Packages table → Quick start → Testing).1228. `task check` must pass green before the first `task tag VERSION=0.1.0`.123124## 14. Out of scope125126- **Deployable services / `cmd/` binaries** — this scaffolds *libraries* consumers wire into their own composition root. Service layout → [go-architect §16](../../languages/go-architect/SKILL.md#16-multi-module-workspaces-gowork).127- **Functional-options APIs, ORMs, viper-in-library** — deliberately rejected by the house style (§5, §11).128- **The Go idioms and linter catalog themselves** — owned by [go-architect](../../languages/go-architect/SKILL.md).