RPC And API Contracts
Purpose
A contract binds for as long as the oldest caller and the oldest stored message live, not for
as long as a deploy takes. The failure this prevents is the change that is correct in the
repository and an outage in the fleet: a renamed field, a narrowed type or a reused field
number shipped as one deploy into a rolling upgrade where both versions are running at once.
The second failure is an error surface designed for a human reading a log. A machine caller
has to decide retry or not, fall back or not, page or not. If that decision requires parsing
English, the client is coupled to your wording and every rephrasing is a breaking change.
Compatibility and evidence
Inspect supported clients, deployed Java/framework/serializer versions, generated artifacts,
actual mapper configuration and rollout/retention policy. The record sketch requires Java
16+; Spring ProblemDetail requires Spring Framework 6+ (Java 17+). Do not upgrade a target
or regenerate every client merely to fit an example. Unknown client inventory or absent
telemetry leaves compatibility unverified; propose how to close the gap without inventing
proof of either compatibility or incompatibility.
Workflow
- Decide what the call actually is before choosing a transport: a synchronous answer, or
an acceptance of work. Long-running synchronous work needs queryable operation identity
or reconciliation for ambiguous timeout; asynchronous acceptance must define status,
retention and eventual failure too.
- Design the error surface around caller decisions. Give stable problem types/codes,
outcome certainty (rejected versus may-have-applied), retry precondition/advice, field
violations and a status/operation URI where applicable. Retry safety composes method
semantics, idempotency key, current state and failure—not one universal boolean.
- Classify every change as additive, compatible-in-one-direction, or breaking, and name
which side may deploy first. See
references/contract-evolution.md.
- Ship a breaking change as expand → migrate → contract — compatibility phases that may
require several deploys. A rename needs a verified transition, not an unqualified edit.
- Prove the coexistence pairs the rollout can create. Test old-reader/new-writer and
new-reader/old-writer where deployment order or durable data permits each. Include retries,
cached/stored payloads, rollback and unknown error/enum values.
- Version only what cannot be made compatible, and emit requests-per-version with a
bounded client category or protected client-level logs; combine telemetry with supported
client inventory and the retirement contract.
Rules
Partial failure is the difference that matters: after dispatch, a timeout/disconnect may mean
the callee applied the effect. The contract must provide a stable operation/idempotency key,
status lookup/reconciliation, or explicitly expose the unresolved outcome (idempotency).
Transport exceptions alone often cannot distinguish lost request from lost response.
Protocol/application evidence can: pre-dispatch failure, durable operation status or a
deduplicated retry. Do not infer peer state from timeout class.
Error codes are a documented extensible set unless the API version promises otherwise.
Known meanings remain stable; clients need a conservative unknown-code path. Human-readable
text is explicitly not branching contract and may be reworded,
localised or redacted without a version change — say so in the documentation, or clients
will parse it anyway.
The response can carry outcome=REJECTED|UNKNOWN, retryCondition, Retry-After or an
operation-status URI. The client combines those with idempotency and deadline. A naked
retryable=true cannot express "refresh state", "same key only" or ambiguity.
RFC 9457 (which obsoletes RFC 7807) defines application/problem+json with type,
title, status, detail and instance. Put the machine-readable members — code,
outcome/retry condition and correlation id — in extension members, never inside detail.
The standard type URI is already the primary problem identifier; code is optional.
In gRPC, mapping every failure to INTERNAL loses semantics, but no status is universally
retryable. UNAVAILABLE may still be ambiguous for a mutation; RESOURCE_EXHAUSTED may be
quota or capacity; ABORTED commonly means retry a higher transaction; DEADLINE_EXCEEDED
can occur after effect. Publish method-specific retry policy and structured details.
Compatibility has a direction, and terminology varies by ecosystem. Define it explicitly:
backward (new reader reads old data) usually permits consumers first; forward (old reader
reads new data) permits producers first. Full compatibility permits arbitrary coexistence;
an ordered rollout can rely on one direction only if rollback and all live/stored pairs are
controlled.
The compatibility horizon is the maximum of live old-client lifetime, rollback window,
broker/cache retention, DLQ/operator replay, backup restore and archived reprocessing.
Required direction can change across rollout phases; it is not always both for retention.
Apply format-specific identity rules: never reuse Protobuf field numbers (reserve removed
numbers/names); JSON/Avro names and aliases differ. Type, validation or domain narrowing is
breaking for values old clients may send/read. Introduce required fields through an
optional/defaulted and negotiated transition.
Proto3 implicit-presence singular scalars conflate absent/default; optional, message fields
and Editions explicit presence preserve it. Check protoc/runtime/API compatibility before
introducing presence into generated clients.
Jackson 2 enables FAIL_ON_UNKNOWN_PROPERTIES by default; Boot 3's auto-configured
Jackson 2 mapper disables it. Jackson 3 defaults it to false. Custom mappers, annotations
and readers can differ; verify the actual client. Unknown properties and unknown enum
values are separate policies.
Retirement needs evidence appropriate to the supported population, including dormant
clients and replay. Zero requests during a short window is not proof of no dependency.
Choose the transport on conditions, without treating style as destiny: gRPC often fits
controlled service clients when deadline/cancellation propagation, generated schemas or
streaming matter; browser/public use requires compatible gateway/tooling. REST/JSON
when the caller set is open or browser-based, intermediary caching matters, or clients
cannot be made to regenerate stubs. REST can stream and gRPC can serve public clients at
additional ecosystem cost. Messaging when the producer must not wait, when
fan-out or replay is required, or when consumer availability must not bound the producer —
at which point the delivery guarantee becomes part of the contract (delivery-semantics).
Propagated deadlines/cancellation do not prove server work or downstream effects stopped.
Define cooperative cleanup and preserve ambiguous outcomes after dispatch.
If-Match uses strong comparison; enforce the precondition atomically with the mutation.
Idempotency keys need authenticated scope, operation, request fingerprint and retention;
coordinate deduplication with effects and define concurrent/unknown completion behavior.
Contract dimensions often omitted
- authentication/authorization scope, tenant isolation and whether existence errors leak data;
- idempotency namespace/retention and operation-status lifecycle for
202 Accepted;
- pagination cursor opacity/stability, filtering/sort semantics and snapshot consistency;
- numeric units/ranges, Unicode, time zone/precision and absent/null/empty distinctions;
- payload/metadata limits, compression, cancellation and deadline propagation;
- cache validators/conditional requests, privacy/redaction and audit requirements;
- rate-limit, deprecation/sunset signals and capability negotiation.
Schema artifacts alone are insufficient: validate invariants, failure semantics and the
actual rollout pairs. Return the changed contract, supported pairs and deployment order,
outcome/retry policy, executed validation and remaining unknowns. Do not equate an untested
pair with a pass.
References
- Contract and schema evolution — the compatibility
matrix with who deploys first, the expand/migrate/contract sequence, the concrete rules for
JSON, Protobuf and Avro, the versioning decision block, and exactly what a consumer-driven
contract test proves and does not. Read before changing any shared message, endpoint or
schema, and before proposing a new version.
- The error contract in Java — a problem-details record with
its contract and non-contract members separated, the single place a status becomes a
decision, the gRPC status mapping table, and how a client acts without matching a string.
Read when designing or reviewing the error surface of an API or client.
1---2name: rpc-and-api-contracts3description: The contract between two services and how it changes without a coordinated deploy: partial failure as a first-class outcome, an error surface a machine caller can act on (stable extensible codes, outcome certainty, retry conditions, RFC 9457), compatibility in both directions and expand-then-contract, versioning only where compatibility is impossible, and choosing REST, gRPC or messaging on observable conditions. Use when a client branches on an error message string, when a field is renamed or a proto field number reused, when a rolling deploy breaks consumers, when a synchronous endpoint fronts a long-running operation, when a new version is proposed for an additive change, or when a consumer fails on an unknown JSON property. Does not cover delivery guarantees (delivery-semantics), the deadline itself (timeouts-and-deadlines), wire-format cost (serialization-performance), the exception hierarchy (java-exception-design), or event contracts (event-driven-architecture).4---56# RPC And API Contracts78## Purpose910A contract binds for as long as the oldest caller and the oldest stored message live, not for11as long as a deploy takes. The failure this prevents is the change that is correct in the12repository and an outage in the fleet: a renamed field, a narrowed type or a reused field13number shipped as one deploy into a rolling upgrade where both versions are running at once.1415The second failure is an error surface designed for a human reading a log. A machine caller16has to decide retry or not, fall back or not, page or not. If that decision requires parsing17English, the client is coupled to your wording and every rephrasing is a breaking change.1819## Compatibility and evidence2021Inspect supported clients, deployed Java/framework/serializer versions, generated artifacts,22actual mapper configuration and rollout/retention policy. The record sketch requires Java2316+; Spring `ProblemDetail` requires Spring Framework 6+ (Java 17+). Do not upgrade a target24or regenerate every client merely to fit an example. Unknown client inventory or absent25telemetry leaves compatibility unverified; propose how to close the gap without inventing26proof of either compatibility or incompatibility.2728## Workflow29301. **Decide what the call actually is** before choosing a transport: a synchronous answer, or31 an acceptance of work. Long-running synchronous work needs queryable operation identity32 or reconciliation for ambiguous timeout; asynchronous acceptance must define status,33 retention and eventual failure too.342. **Design the error surface around caller decisions.** Give stable problem types/codes,35 outcome certainty (rejected versus may-have-applied), retry precondition/advice, field36 violations and a status/operation URI where applicable. Retry safety composes method37 semantics, idempotency key, current state and failure—not one universal boolean.383. **Classify every change** as additive, compatible-in-one-direction, or breaking, and name39 which side may deploy first. See `references/contract-evolution.md`.404. **Ship a breaking change as expand → migrate → contract** — compatibility phases that may41 require several deploys. A rename needs a verified transition, not an unqualified edit.425. **Prove the coexistence pairs the rollout can create.** Test old-reader/new-writer and43 new-reader/old-writer where deployment order or durable data permits each. Include retries,44 cached/stored payloads, rollback and unknown error/enum values.456. **Version only what cannot be made compatible**, and emit requests-per-version with a46 bounded client category or protected client-level logs; combine telemetry with supported47 client inventory and the retirement contract.4849## Rules5051- Partial failure is the difference that matters: after dispatch, a timeout/disconnect may mean52 the callee applied the effect. The contract must provide a stable operation/idempotency key,53 status lookup/reconciliation, or explicitly expose the unresolved outcome (`idempotency`).54- Transport exceptions alone often cannot distinguish lost request from lost response.55 Protocol/application evidence can: pre-dispatch failure, durable operation status or a56 deduplicated retry. Do not infer peer state from timeout class.57- Error codes are a documented **extensible** set unless the API version promises otherwise.58 Known meanings remain stable; clients need a conservative unknown-code path. Human-readable59 text is explicitly **not** branching contract and may be reworded,60 localised or redacted without a version change — say so in the documentation, or clients61 will parse it anyway.62- The response can carry `outcome=REJECTED|UNKNOWN`, `retryCondition`, `Retry-After` or an63 operation-status URI. The client combines those with idempotency and deadline. A naked64 `retryable=true` cannot express "refresh state", "same key only" or ambiguity.65- RFC 9457 (which obsoletes RFC 7807) defines `application/problem+json` with `type`,66 `title`, `status`, `detail` and `instance`. Put the machine-readable members — code,67 outcome/retry condition and correlation id — in extension members, never inside `detail`.68 The standard `type` URI is already the primary problem identifier; `code` is optional.69- In gRPC, mapping every failure to `INTERNAL` loses semantics, but no status is universally70 retryable. `UNAVAILABLE` may still be ambiguous for a mutation; `RESOURCE_EXHAUSTED` may be71 quota or capacity; `ABORTED` commonly means retry a higher transaction; `DEADLINE_EXCEEDED`72 can occur after effect. Publish method-specific retry policy and structured details.73- Compatibility has a direction, and terminology varies by ecosystem. Define it explicitly:74 backward (new reader reads old data) usually permits consumers first; forward (old reader75 reads new data) permits producers first. Full compatibility permits arbitrary coexistence;76 an ordered rollout can rely on one direction only if rollback and all live/stored pairs are77 controlled.78- The compatibility horizon is the maximum of live old-client lifetime, rollback window,79 broker/cache retention, DLQ/operator replay, backup restore and archived reprocessing.80 Required direction can change across rollout phases; it is not always both for retention.81- Apply format-specific identity rules: never reuse Protobuf field numbers (reserve removed82 numbers/names); JSON/Avro names and aliases differ. Type, validation or domain narrowing is83 breaking for values old clients may send/read. Introduce required fields through an84 optional/defaulted and negotiated transition.85- Proto3 implicit-presence singular scalars conflate absent/default; `optional`, message fields86 and Editions explicit presence preserve it. Check protoc/runtime/API compatibility before87 introducing presence into generated clients.88- Jackson 2 enables `FAIL_ON_UNKNOWN_PROPERTIES` by default; Boot 3's auto-configured89 Jackson 2 mapper disables it. Jackson 3 defaults it to false. Custom mappers, annotations90 and readers can differ; verify the actual client. Unknown properties and unknown enum91 values are separate policies.92- Retirement needs evidence appropriate to the supported population, including dormant93 clients and replay. Zero requests during a short window is not proof of no dependency.94- Choose the transport on conditions, without treating style as destiny: **gRPC** often fits95 controlled service clients when deadline/cancellation propagation, generated schemas or96 streaming matter; browser/public use requires compatible gateway/tooling. **REST/JSON**97 when the caller set is open or browser-based, intermediary caching matters, or clients98 cannot be made to regenerate stubs. REST can stream and gRPC can serve public clients at99 additional ecosystem cost. **Messaging** when the producer must not wait, when100 fan-out or replay is required, or when consumer availability must not bound the producer —101 at which point the delivery guarantee becomes part of the contract (delivery-semantics).102103- Propagated deadlines/cancellation do not prove server work or downstream effects stopped.104 Define cooperative cleanup and preserve ambiguous outcomes after dispatch.105- `If-Match` uses strong comparison; enforce the precondition atomically with the mutation.106 Idempotency keys need authenticated scope, operation, request fingerprint and retention;107 coordinate deduplication with effects and define concurrent/unknown completion behavior.108109## Contract dimensions often omitted110111- authentication/authorization scope, tenant isolation and whether existence errors leak data;112- idempotency namespace/retention and operation-status lifecycle for `202 Accepted`;113- pagination cursor opacity/stability, filtering/sort semantics and snapshot consistency;114- numeric units/ranges, Unicode, time zone/precision and absent/null/empty distinctions;115- payload/metadata limits, compression, cancellation and deadline propagation;116- cache validators/conditional requests, privacy/redaction and audit requirements;117- rate-limit, deprecation/sunset signals and capability negotiation.118119Schema artifacts alone are insufficient: validate invariants, failure semantics and the120actual rollout pairs. Return the changed contract, supported pairs and deployment order,121outcome/retry policy, executed validation and remaining unknowns. Do not equate an untested122pair with a pass.123124## References125126- [Contract and schema evolution](references/contract-evolution.md) — the compatibility127 matrix with who deploys first, the expand/migrate/contract sequence, the concrete rules for128 JSON, Protobuf and Avro, the versioning decision block, and exactly what a consumer-driven129 contract test proves and does not. Read before changing any shared message, endpoint or130 schema, and before proposing a new version.131- [The error contract in Java](references/error-contract.md) — a problem-details record with132 its contract and non-contract members separated, the single place a status becomes a133 decision, the gRPC status mapping table, and how a client acts without matching a string.134 Read when designing or reviewing the error surface of an API or client.