# 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.