Versioning & Evolution
Your API will outlive its first shape
Section titled “Your API will outlive its first shape”The protobuf module showed how the wire format tolerates change — field numbers, not names, identify data. This lesson is about the discipline that turns that low-level tolerance into a real versioning strategy your callers can rely on.
The golden rule: old clients and new servers must keep working, and new clients and old servers must keep working. Everything below serves that.
Version lives in the package
Section titled “Version lives in the package”The convention is to put the major version in the protobuf package (and the file path):
package user.v1; // user/v1/user.protoWhen you need incompatible changes, you don’t mutate v1 — you create user.v2 alongside it. Generated code lands in separate namespaces, so v1 and v2 can run in the same server at the same time, and callers migrate on their own schedule.
Additive vs breaking
Section titled “Additive vs breaking”Most changes should be additive — safe because the wire format ignores what it doesn’t recognize:
| Safe (additive) | Breaking (needs a new version) |
|---|---|
| Add a new field with a new number | Change a field’s number |
| Add a new method to a service | Change a field’s type |
| Add a new enum value (usually) | Rename/reuse a removed field number |
| Add a new message | Remove a field still in use |
| Rename a package, service, or method |
flowchart TB v1["user.v1"] -->|add field #7, add method| v1b["user.v1 (still compatible)"] v1 -->|change field type, remove field| v2["user.v2 (new package, runs alongside v1)"] client1["old client"] --> v1b client2["new client"] --> v2
Reserve what you retire
Section titled “Reserve what you retire”When you do remove a field, never let its number (or name) be reused — a future field with the same number would silently misread old data. Mark it reserved:
message User { reserved 4, 7; reserved "phone_number"; int64 id = 1; string name = 2;}This is the schema-level guardrail behind the field-number rule: the compiler now refuses to reassign 4, 7, or phone_number.
Let a tool enforce it
Section titled “Let a tool enforce it”Humans miss breaking changes in review. buf breaking compares your .proto against a baseline (the last released version, a git ref, or a registry) and fails CI if a change would break the wire or generated code:
buf breaking --against '.git#branch=main'Wiring this into CI turns “please remember all the compatibility rules” into an automated gate — the single highest-leverage habit for a schema many teams depend on.