Files
core/docs/adr/ADR-001-core-module-composition.md
Rene Nochebuena 38a415c2ab feat(core): initial implementation — launcher, logz, xerrors, valid
Introduces `code.nochebuena.dev/einherjar/core` — the foundational implementation
module of the Einherjar framework. Provides four sub-packages that together cover
every service's baseline needs: lifecycle management, structured logging, typed
errors, and struct validation.

- launcher: Launcher interface — three-phase managed lifecycle (OnInit → BeforeStart
  hooks → OnStart → OS signal wait → OnStop in reverse). Accepts
  lifecycle.Component and logging.Logger from contracts. Prints an ASCII art banner
  at startup (EINHERJAR_BANNER=off to suppress). Banner includes core version via
  runtime/debug.ReadBuildInfo() and a loaded-module list for every registered
  component that implements observability.Identifiable. Config struct with
  EINHERJAR_COMPONENT_STOP_TIMEOUT env tag (caarlos0/env syntax, default 15s).

- logz: Logger implementation backed by log/slog. Returns contracts/logging.Logger.
  Detects errs.CodedError and errs.ContextualError (from contracts/errs) to enrich
  log records automatically — replaces the private duck-typed bridge from micro-lib.
  Context helpers: WithRequestID, WithField, WithFields, GetRequestID. Config struct
  with EINHERJAR_LOG_LEVEL (default INFO) and EINHERJAR_LOG_JSON (default false) env
  tags (caarlos0/env syntax); programmatic-only fields StaticArgs and Writer carry no
  tags.

- xerrors: Typed error codes with context enrichment. Complete gRPC canonical set
  (16 codes) plus HTTP 410 ErrGone. Adds ErrOutOfRange, ErrAborted, ErrDataLoss
  over micro-lib. One convenience constructor per code. *Err declares compile-time
  satisfaction of errs.CodedError and errs.ContextualError.

- valid: Struct validation wrapping go-playground/validator/v10. Validator interface
  + MessageProvider interface with full built-in tag coverage (~150 tags) in both
  DefaultMessages (English) and SpanishMessages (Spanish). Backend fully hidden;
  returns *xerrors.Err with ErrInvalidInput or ErrInternal. FieldLevel interface
  abstracts the backend's field-level access for custom validators.
  WithCustomValidator registers custom validation tags at construction time;
  OverrideProvider chains a tag→handler map with a fallback MessageProvider for
  custom tag messages without re-implementing built-ins.

Compliance test enforces CT-6 (at most one exported TypeSpec per file via AST) and
verifies behavioural correctness of all four sub-packages, including custom validator
registration and OverrideProvider composition. Compile-time var _ assertions prove
interface satisfaction.

docs: ADR-001 (core module composition), ADR-002 (logz contracts/errs adoption),
ADR-003 (Config naming convention and caarlos0/env tag standard)
2026-05-29 15:45:12 +00:00

3.3 KiB

ADR-001: core module composition — four sub-packages in one Go module

  • Date: 2026-05-28
  • Module: code.nochebuena.dev/einherjar/core
  • Status: Accepted

Context

In micro-lib, the four packages that become core are independent Go modules:

  • code.nochebuena.dev/go/launcher
  • code.nochebuena.dev/go/logz
  • code.nochebuena.dev/go/xerrors
  • code.nochebuena.dev/go/valid

Each has its own go.mod, version tag, and release cycle. A dependency update to xerrors requires a version bump on xerrors, then updating valid to consume the new version, then informing all callers of both. Four packages → four coordinated version bumps per change.

In the Einherjar module structure, each entry in the module inventory is one independently versionable unit. The Spring Boot starter model places the distribution boundary at the starter level (db-postgres, cache-valkey, auth), not at the foundational implementation level.

Decision

launcher, logz, xerrors, and valid are sub-packages of a single code.nochebuena.dev/einherjar/core Go module.

Rationale

They always ship together. Every Einherjar service needs all four:

  • launcher requires a logging.Logger — it calls logz.New in the same main function
  • valid returns *xerrors.Err — they are semantically coupled at the type level
  • No known use case requires logz without xerrors, or launcher without logz

Version coordination cost is real. With four separate modules, a one-line fix to xerrors becomes four coordinated operations: tag xerrors, update valid's go.mod, tag valid, inform consumers. With one module, it is one operation.

The Spring Boot analogy holds at the right level. Spring Boot's parent POM bundles dozens of interdependent libraries into a single versioned unit. Starters (spring-boot-starter-data-jpa) are the distribution boundary, not the individual spring-data-commons jar inside them. Einherjar's core is the parent; the starters are the distribution units.

Alternatives Considered

Four separate modules (core-launcher, core-logz, etc.). Rejected — over-engineering at the foundational layer. There is no consumer that needs logz but not xerrors, or launcher but not logz. The additional version coordination overhead has no compensating benefit.

One flat core package (no sub-packages). Rejected — importing core would pull all four concerns into any module that only needs errors or validation. Sub-packages preserve the ability to import only what is needed.

Consequences

Easier: A single go get code.nochebuena.dev/einherjar/core@v1.x.x installs everything. A single version bump covers all four sub-packages. Dependency graph for downstream starters stays simple.

Harder: A change to any sub-package bumps the entire core version, even if the change is isolated to valid. This is the accepted trade-off — the coupling is real; the version increment reflects it.

New obligations: Sub-packages within core may import each other in one direction: valid may import xerrors; nothing else crosses package boundaries within core. Circular imports between sub-packages are prohibited. launcher uses contracts/logging — not core/logz directly — to preserve the contracts layer.