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

126 lines
4.8 KiB
Markdown

# 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.](../diagrams/score-ref-composition.svg)](../diagrams/score-ref-composition.svg)
```rust
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:
```rust
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.](../diagrams/score-ref-cycle.svg)](../diagrams/score-ref-cycle.svg)
```rust
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](../adr/028-typed-score-references.md) for the decision and the
[Fleet reference](../reference/fleet-score-references.md) for the first application.