3.9 KiB
ADR-0004: Package modules by domain, not by stage
Status: Accepted Date: 2026-07-13
Context
Module packages can be grouped first by pipeline stage, such as
modules/chunk/dnd/scenes, or first by domain, such as
modules/dnd/chunk/scenes. A domain's extract, merge, normalize, validation,
schema, prompt, and artifact-codec implementations collaborate around the same
artifact types and are likely to evolve together.
Go package dependencies also constrain registration. If shared types live in a domain root package, that package cannot import child implementation packages to register them because the children already import the root types.
Decision
Production extensions are grouped by domain under:
internal/modules/<domain>/<stage>/<name>
Shared artifact types live at the domain root, for example
internal/modules/dnd/types.go. Domain-specific validators, prompt fragments,
schemas, reference helpers, and codecs also live within that domain tree.
Each domain exposes one production registration entry point from a sibling
registrar package, for example internal/modules/dnd/register. The registrar
may import the domain root and its child implementations; the domain root does
not import its registrar or child packages. This keeps shared types available
as dnd.SpellList without creating a Go import cycle.
The generic tree is a peer extension family for reusable implementations that
contain no concrete source-format or artifact-domain knowledge. Source-format
and output-format families, such as Seriatim and JSON output, follow the same
domain-first organization even when they do not define a type in the
domain artifact zone.
Concrete domain implementation packages do not import another concrete domain. Generic extension packages never import concrete domains. A domain registrar may import domain-neutral generic extension packages to instantiate a reusable strategy for that domain's artifact type; the generic implementation remains unaware of the concrete type's domain semantics. Reuse needed directly by a domain implementation lives in a domain-neutral framework or helper package, not in a peer extension package.
The application composition root may import multiple registrar packages, and black-box integration tests may compose multiple domains. Other cross-domain reuse occurs through engine contracts and composition-time registration rather than concrete peer-domain imports.
A domain registrar owns registration of that domain's modules, validators, default validator chains, artifact codecs, schemas, and prompt assets. It does not take ownership of application execution or process behavior.
Alternatives considered
- Group modules by stage. This keeps interchangeable strategies side by side, but scatters a domain's shared artifact model and collaborating extensions across the repository. It is preferable when generic strategy libraries dominate or when the project is primarily a stage-extension framework rather than an application composed from domain suites.
- Put both shared types and
Registerin the domain root. This gives the shortest import path but creates an import cycle once child implementations import the root artifact types.
Consequences
The repository layout makes supported domains immediately visible, and adding or extracting a domain affects one cohesive subtree. The CLI composition root depends on a small set of domain registrars instead of every leaf package.
Package moves must preserve user-visible module and validator keys unless a separate compatibility decision changes them. Shared behavior that cannot be expressed through framework contracts may need to move into a domain-neutral framework package rather than creating a concrete peer-domain import. Registrar packages become explicit composition points for instantiating generic typed strategies, in addition to registering domain-owned implementations.