Skip to content
GitHub
Decisions

ADR-071: Application-layer use case handlers placed flat under `<context>.application.<domain>`

Status: Accepted (2026-05-01)

Context

ADR-031 establishes the single-library + per-context namespace convention (spectral.<context>.*) but leaves the internal organization of each context’s application layer unspecified. During the M10 wave-5 re-refinement (2026-04-30), four epics — SPEC-238 (World Agent + evolution loop), SPEC-254 (operator world-model authoring), SPEC-255 (source-material distillation), SPEC-256 (version publication + release notes) — converged on a flat per-domain layout for application-layer use case handlers:

  • worlds.application.world_models.publish — not worlds.application.world_models.use_cases.publish
  • worlds.application.publication.atomic_mint — not worlds.application.publication.commands.atomic_mint
  • worlds.application.distillation.run_llm_pass — not worlds.application.distillation.handlers.run_llm_pass

The convergence was implicit — each epic chose flat independently based on Tier 1 use case handler simplicity per ADR-070 — but never pinned as doctrine. SPEC-237 and SPEC-239 (refined pre-M10) carry implicit nesting in their AC text. Propagating the convention to those epics needs a written-down decision rather than continued implicit convergence.

This ADR pins the convention.

Decision

D1 — Flat module per domain

Application-layer use case handlers live in flat modules under <context>.application.<domain>. Each domain has one module path — not a nested package with use-case sub-modules.

Concrete shape:

spectral/
└── worlds/
└── application/
├── world_models/
│ ├── __init__.py
│ ├── publish.py # PublishWorldModel use case handler
│ ├── archive.py # ArchiveWorldModel use case handler
│ └── read_health.py # ReadWorldModelHealth use case handler
├── rules/
│ ├── __init__.py
│ ├── enshrine.py
│ ├── revise.py
│ └── cluster_link.py
└── publication/
├── __init__.py
├── atomic_mint.py
└── release_notes.py

Each handler module exports its handler class or function as a top-level symbol. Tests and adapters import directly: from spectral.worlds.application.world_models.publish import PublishWorldModelHandler.

D2 — Nesting permitted when a domain has internal substructure

The flat default is not a ban. If a domain’s handler set genuinely benefits from internal grouping — typically when the count exceeds ~6–8 handlers and natural sub-categories emerge — nested sub-packages are permitted. Example: if rules later splits into authoring vs lifecycle vs querying with 4–5 handlers each, a nested layout (worlds.application.rules.authoring.*, worlds.application.rules.lifecycle.*) is fine.

The default is flat because most domains hold 2–5 handlers, and flat reduces directory noise + import-path length. Nesting is the escape hatch, not the convention.

D3 — Independent of contracts placement

Inter-context contract surfaces follow ADR-065 D2 / D3 (producer-typed payloads at <context>.contracts.events.*, callee-owned Protocols at <context>.contracts.protocols.*). Those locations are independent of the application-layer organization pinned here. ADR-065 covers the contracts boundary; this ADR covers the application-layer internal layout.

Alternatives considered

Nested under use_cases / commands / handlers sub-modules. Rejected. Adds one directory level + one __init__.py per domain with no semantic gain — every module under <context>.application.<domain> IS a use case handler by definition; the sub-package label is redundant. Imports become worlds.application.world_models.use_cases.publish instead of worlds.application.world_models.publish — three name parts where two carry the same information.

Per-handler sub-package (e.g., worlds.application.world_models.publish_world_model.handler). Rejected. Useful when a handler ships with adjacent test fixtures, mocks, or DTOs that warrant package-scoped colocation. In practice, handlers in this codebase are single-file with tests in tests/ — the per-handler package is empty overhead.

Consequences

  • SPEC-237 + SPEC-239 patch direction. Pre-M10 AC text using nested module paths aligns to flat per this ADR. Lands in the patch-sweep workstream alongside the other DIRTY-WITH-PATCHES sweeps.
  • Future epics default flat. Application-layer module paths in epic ACs use flat layouts unless a domain-internal-substructure case is made explicitly.
  • Validator rule available. The architecture validator can add a rule rejecting */use_cases/*.py, */commands/*.py, and */handlers/*.py paths under <context>/application/ to prevent drift. Add to the validator’s next-touch (SPEC-235 / kernel-audit cadence).
  • Independent of contracts placement. ADR-065 D2/D3 placements are unaffected; this ADR addresses application-layer internals only.

References

  • ADR-031 — single library + per-context namespace; this ADR fills in the application-layer internal convention.
  • ADR-065 — contracts boundary; orthogonal to this ADR’s scope.
  • ADR-070 — Tier 1 use case handler default; the simplest-fit choice that drove M10 wave-5 epics toward flat layouts.
  • SPEC-238, SPEC-254, SPEC-255, SPEC-256 — M10 wave-5 epics that converged on the flat layout.
  • SPEC-237, SPEC-239 — patch targets for alignment per the audit-A disposition.