Skip to content

Versioning & Evolution

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.

The convention is to put the major version in the protobuf package (and the file path):

package user.v1; // user/v1/user.proto

When 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.

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 numberChange a field’s number
Add a new method to a serviceChange a field’s type
Add a new enum value (usually)Rename/reuse a removed field number
Add a new messageRemove 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
Additive change stays in v1; incompatible change earns a v2

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.

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:

Terminal window
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.

Where does the convention put an API major version?
Which change is safe (additive) and does NOT require a new version?
Why do you reserve a removed field's number?
What does running `buf breaking` in CI achieve?