Skip to content

docs(architecture): ADR 0003 — API design over backwards compatibility until 1.0 - #176

Merged
sksizer merged 1 commit into
mainfrom
docs/adr-0003-api-design-over-compat
Sep 26, 2026
Merged

sksizer merged 1 commit into
mainfrom
docs/adr-0003-api-design-over-compat

Conversation

@sksizer

@sksizer sksizer commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Writes down a rule that has been decided case-by-case until now, and the corollary that gives it teeth.

The rule

Until 1.0 — API design trumps backwards compatibility. A cleaner surface justifies a break. No shims, no transitional aliases, no foo2() beside foo().

From 1.0 — the ability to evolve the API is itself part of the API. A surface that cannot absorb a new field, variant or mode without a major bump is not finished, however clean it looks.

Why they are one decision and not two

Installing an evolvability mechanism is itself a breaking change. Per the Rust reference, a #[non_exhaustive] type "cannot be constructed with a StructExpression (including with functional update syntax)" — so adding it breaks every consumer literal and every ..base update.

It can therefore only be fitted while breaking is free. Phase 2 is affordable only if phase 1 is spent installing the hinges. The pre-1.0 window is not just for getting the shape right; it is for fitting the joints the shape will later need to bend at.

What forced the question

Two forces had been pulling against each other with nothing to arbitrate:

  • Semver tooling pushes toward preservation. cargo-semver-checks flagged two additive improvements on chore: release #174 — constructible_struct_adds_field for EntityDef.doc/FieldDef.doc, enum_variant_added for CodegenError::Docs. Every such flag creates quiet pressure to contort the design to keep the check green.
  • Design review pushes toward re-cutting. The pagination review found the feature declared in four config structs, two of which can silently diverge. Later exploration widened it: ServersConfig and ClientsConfig duplicate twelve fields, and both stages independently scan their own copy of api_dir. Pagination is one instance of a bug class with twelve.

With no rule, the decision defaults to whichever is locally cheaper — always preservation, since it requires no work. That default is how the divergence accreted. Nobody chose it; it is the residue of never having chosen.

Notable contents

  • A hinge table — the six mechanisms to install before 1.0. Three were already applied in the pagination design before this ADR named them (open-string mode, the nested list capability object, reserved order_by/order slots), which is the evidence the instinct was sound.
  • semver_check = true explicitly stays. The rule is "do not avoid breaks," not "do not detect them" — the check drives the version bump rather than a redesign. Dropping it is listed as a rejected alternative because it is the tempting misreading.
  • Blast-radius asymmetry is named. A build-time break is caught at compile time; a generated-protocol break fails at runtime for already-deployed clients. Same freedom, different consequence — so protocol breaks get flagged in the changelog as deploy-ordering hazards. Live immediately: 0.8.0 changes the list envelope.
  • An owed obligation: a pre-1.0 evolvability sweep, deadlined at 1.0. Its failure mode is silent expiry, not visible overdue-ness.

Also

Replaces the stale "None written yet" line in the ADR README with an index, and records 0002 as reserved by T-66TG per the numbering convention.

Review focus

The decision itself, not the prose. Two calls made without explicit sign-off, both easy to reverse: extending the rule to the generated wire protocol (with the asymmetry called out rather than a separate policy), and leaving the sweep untracked pending a decision on where it should live.

…y until 1.0

Records the rule that has been decided case-by-case until now, and the
corollary that gives it teeth.

Two phases: until 1.0, a cleaner surface justifies a break, with no shims
or transitional aliases; from 1.0, evolvability becomes a design criterion
in its own right.

The corollary is what links them. Installing an evolvability mechanism is
itself a breaking change — `#[non_exhaustive]` blocks struct literals and
`..base` functional updates alike — so the hinges can only be fitted while
breaking is still free. Phase 2 is affordable only if phase 1 is spent
installing them.

Context for the decision: cargo-semver-checks flagged two additive changes
on #174 (`constructible_struct_adds_field`, `enum_variant_added`), which
creates standing pressure to contort the design to stay compatible. In the
absence of a rule that pressure wins by default, because preservation is
the option that requires no work. That default is how pagination ended up
declared in four config structs, two of which can silently diverge.

Also replaces the stale "none written yet" line in the ADR README with an
index, and reserves 0002 for T-66TG per the numbering convention.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying ontogen with  Cloudflare Pages  Cloudflare Pages

Latest commit: 67749f8
Status: ✅  Deploy successful!
Preview URL: https://ac1401f3.ontogen.pages.dev
Branch Preview URL: https://docs-adr-0003-api-design-ove.ontogen.pages.dev

View logs

@sksizer
sksizer merged commit 6a301c4 into main Sep 26, 2026
2 checks passed
@sksizer
sksizer deleted the docs/adr-0003-api-design-over-compat branch September 26, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant