Files
harmony/docs/concepts/score-references.md
Jean-Gabriel Gill-Couture b63de408f1
All checks were successful
Run Check Script / check (pull_request) Successful in 1m55s
docs: Add code samples and diagrams to score ref docs
2026-07-15 18:33:41 -04:00

4.8 KiB

Typed Score references

Harmony builds loosely dependent but strongly typed infrastructure components using Refs.

This means that a ZitadelScore does not directly depend on a PostgreSQLScore. It can instead accept a PostgreSQLRootAccountRef in its builder to know how to connect to PostgreSQL. This is safer than passing strings around: the Ref has a type Rust can check, and consumers do not need to parse or reconstruct the producer's naming conventions.

The loose dependency matters. Previous design proposals treated composition as a dependency graph or pipeline, which forced components to know too much about one another. A Ref is only a typed reference to declarative desired state. It does not give the consumer control over the producer's lifecycle.

Most importantly, a Ref does not guarantee that its target exists or has converged when it is passed. Like a Kubernetes reference, it is eventually consistent. Consumers must tolerate the target being absent, starting, or temporarily unavailable.

Kubernetes gets this right: infrastructure is chaotic. A component can fail after it becomes ready. Drives fail, power goes out, and networks split. The long-term answer is continuous convergence, not an assumption that deployment finishes infrastructure forever.

The convergence mechanism is outside ADR-028. Choosing eventually consistent Refs now lets Scores declare relationships without committing Harmony to a scheduler, dependency engine, or reconciliation design.

The whole pattern

SomeDbScore produces DbRef for AppScore.

pub struct DbRef {
    // Desired-state properties needed to connect.
}

let db_score = SomeDbScore::new(...);
let some_db_ref = db_score.connection();
let app_score = AppScore::new(...).with_db(&some_db_ref);

That is the mechanism. connection() derives DbRef from the database's desired state. It performs no I/O and does not require SomeDbScore to have been interpreted.

AppScore knows DbRef. It does not need to know SomeDbScore, its provider, or its readiness procedure.

Compare that with copying a connection string:

app.database_url("postgres://some-db.apps.svc:5432/app");

That string duplicates database naming and connection knowledge in the consumer. A rename or layout change can leave valid Rust that fails at runtime. A typed Ref removes that string contract and makes type-level contract changes visible to the compiler.

Cyclic relationships

A cycle is the same pattern in both directions:

BrokerScore and AuthScore exchange BrokerRef and AuthRef before either Score runs.

let broker_score = BrokerScore::new(...);
let broker_ref = broker_score.connection();

let auth_score = AuthScore::new(...).with_broker(&broker_ref);
let auth_ref = auth_score.endpoint();

let broker_score = broker_score.with_auth(&auth_ref);

No Score has run. Each Ref comes from desired state, so the two relationships can be declared without traversing a graph or resolving either resource.

Eventual consistency

Composition code passes Refs; it does not wait for them to become ready. The consumer's Interpret/Topology path handles an unresolved target in the way appropriate for that component. It may:

  • emit a native Kubernetes reference and let Kubernetes reconcile it;
  • wait or retry when the value is needed immediately;
  • return a non-success outcome for the current interpretation attempt.

ADR-028 does not choose among those behaviors or define when another attempt runs. It only requires consumers not to treat possession of a Ref as proof of availability.

Refs may also identify values generated later, such as an OIDC client ID. The same rule applies: passing the Ref does not claim that the value exists yet.

What types guarantee

Concrete types such as PostgreSQLRootAccountRef, OidcApplicationRef, and NatsAccountRef prevent unrelated references from being exchanged by accident. They also remove string parsing and copied naming rules from consumers.

Types do not prove that:

  • the referenced resource exists;
  • the producer has converged;
  • two live systems can communicate;
  • credentials are authorized;
  • the infrastructure will remain available.

Those remain runtime concerns.

Authoring rule

Use a Ref when one Score would otherwise copy a name, URL, ID, credential location, or other desired-state identifier owned by another component. Use an ordinary typed value when there is no such relationship.

Refs express relationships. They do not impose execution order, create a pipeline, or make Harmony responsible for continuous convergence.

See ADR-028 for the decision and the Fleet reference for the first application.