| name | go-library-builder |
| version | 1.0.0 |
| description | 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. |
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.