Files
harmony/docs/adr/029-application-components.md
Jean-Gabriel Gill-Couture eea006535b refactor: drop harmony_app landfill
Scores own deploy. harmony_app is identity, images, context, and
HarmonyApp→Scores. Authoring DX lives in dx. Compose/chart/Application
model and the Helm render test are gone.
2026-09-08 22:10:34 -04:00

4.5 KiB

Architecture Decision Record: Application Components — One File, One Type

Initial Author: Jean-Gabriel Gill-Couture

Initial Date: 2026-09-03

Last Updated Date: 2026-09-03

Status

Proposed.

Supersedes the app-facing use of a single Application graph (harmony_app::application) as how an application is structured. Does not replace ADR-023 (framework Scores) or ADR-028 (typed Refs).

Context

An application is a map of components someone can point at. The crate should be that map. Earlier shapes hid it: one god-object Application, a type per runtime (FrontendCommand / FrontendContainer), both image and command on one blob checked later, a typestate builder that encodes bind order (a DAG — and real systems cycle, ADR-028).

Decision

One component, one file, one type. Opening frontend.rs is opening the frontend.

Runtimes are capabilities on that type, not sibling types:

  • Container — image on a cluster
  • Command — a process (the command is a string in the impl)
  • Remote — this context does not run it; it only names where it lives

A database that is always on the cluster implements Container only. A frontend that is a container in one context and a process in another implements both. The struct is still Frontend.

A context binds each component to one runtime that component implements. That bind is an ordinary struct the author fills in (frontend: app.frontend.as_command(), …) — missing a field or calling as_command() on something with no Command impl does not compile. Not a macro: macros are cryptic if Rust is not your first language. Refs whose content depends on how it runs (public origin vs localhost) live on that runtime impl. Contexts are user-declared. Harmony may ship default bindings as examples, not as a closed enum.

Components exchange typed Refs (ADR-028). A Ref is desired state, valid before either side has run. Cycles are two Refs, not two builder stages.

A new framework Score exists only when Harmony should orchestrate a reusable capability. App glue is ordinary code in the component file. Org-specific Scores live in that org's crate, not in harmony.

Planned framework Score: OCI/image — Dockerfile in config, build/push from the context, export ContainerRef for K8sDeploymentScore. Until then, build/push may be procedural and still yield a ContainerRef.

Principles

  • Feldman: illegal states don't compile (Command on Postgres, Mailhog in Production, CalloutRef where IssuerRef is required). Runtime is not a boolean on a god object.
  • Crichton: file = mental object; you can reason about frontend.rs without the mesh. Notation matches: impl Command for Frontend is "we run it with a command".
  • Parse, don't validate: no .image + .command on one blob checked later. Missing impl Command is the parse.
  • SOLID: SRP = one file; ISP = Postgres doesn't see Command; DIP = others depend on refs; OCP = a new context is new impls on context-dependent components only; LSP = Frontend stays Frontend, refs stay UrlRef.

Ugly we accept: N named impl Production / LocalDev / Staging on dual-runtime files. Better than a mesh-wide enum or a typestate DAG.

Ugly we refuse: a second type per runtime; context as if local inside scores; builder order as types.

Harmony's job: Container, Command, Remote, Unit, scores, refs.

App's job: one file per component, impl the runtimes it actually has, impl the contexts that bind them.

A process launcher (devbox run, cargo watch, …) is a string inside impl Command for Frontend, never a Harmony type.

Consequences

  • The crate layout is the documentation — five components or tens.
  • Always-cluster components keep a single Container impl.
  • The Application builder was deleted. Apps implement HarmonyApp and compose Scores.
  • --context (ADR-026) selects bindings. It does not rename types.

Alternatives considered

Single Application declaration. One lowering to Scores. The mental model sits behind a god object.

Typestate builder. Readable as a script; types encode order. Breaks cycles; Harmony would have to know the app's slots.

Dual types per runtime. The author no longer has one frontend.

Closed context enum in Harmony. Defaults become the product. Applications cannot declare their own tying-together.

Additional Notes

Related: ADR-023, ADR-026, ADR-028.

First implementation: harmony_app::dx (Command / Container / Remote, as_command / as_container, Slot / Ref), HostProcessScore.