gRPC Schema Evolution

Software never stands still. Teams add features, retire old fields, and fix design mistakes while older clients still run in the wild. Schema evolution means changing a proto file without breaking those running clients and servers.

Why Field Numbers Matter

Protocol Buffers identify fields by number on the wire, never by name. Think of a school roll call: students answer to roll number 7, whatever name the teacher writes on the board. Rename the student and nothing breaks. Give roll number 7 to a different student and chaos follows.

Wire message:  [field 1: "Asha"] [field 2: 31]

Server v1 reads:   1 = name,  2 = age   ✔
Server v2 reads:   1 = name,  2 = age,  3 = email (new)   ✔
Old client sends no field 3 → server uses default

Safe Changes

  • Add a new field with a fresh field number
  • Rename a field, because the wire never carries names
  • Add a new enum value
  • Add a new RPC method to a service
  • Add a new message type

Old readers skip unknown fields without errors. New readers use default values for fields that old writers never sent.

Breaking Changes

ChangeProblem
Change a field numberReaders decode data into the wrong field
Change a field typeWire formats differ and decoding fails or corrupts data
Reuse a deleted numberOld data lands in a new field
Remove an RPC methodOld clients receive UNIMPLEMENTED errors
Change a method request typeMessages no longer match

Retiring Fields with Reserved

Never delete a field and walk away. Mark its number and name as reserved. The compiler then blocks anyone from reusing them.

message Product {
  reserved 4, 7;
  reserved "old_price", "legacy_code";

  string id          = 1;
  string name        = 2;
  int64  price_cents = 3;
  string category    = 5;
}

Deprecating Before Removing

Give clients time to move. Mark the field as deprecated first and remove it after every client stops using it.

message User {
  string id = 1;
  string full_name = 2;
  string nickname = 3 [deprecated = true];
}

Rollout Order

Deploy changes in a safe order so that no version meets a message it cannot read.

  1. Update the proto file with the new field.
  2. Deploy servers that understand the new field.
  3. Deploy clients that start sending the new field.
  4. Remove old behavior after monitoring shows no usage.
Time ───────────────────────────────────────────▶
Server:  v1 ──────── v2 (accepts new field) ─────────
Client:  v1 ─────────────── v2 (sends new field) ────
                     ▲
          Both versions work together here

Versioning with Packages

Some changes cannot stay compatible. Create a new package version and run both versions side by side.

package shop.orders.v1;   // existing clients
package shop.orders.v2;   // new design, new package

The full method names differ: /shop.orders.v1.OrderService/GetOrder and /shop.orders.v2.OrderService/GetOrder. One server hosts both services and retires v1 after clients migrate.

Automatic Checks

The buf tool includes a breaking change detector. It compares your current proto files against the last released version and fails the build when a change breaks compatibility. Add that check to your pipeline so reviewers never need to spot problems by eye.

buf breaking --against ".git#branch=main"

Summary

Field numbers form the permanent identity of data. Add fields freely, never reuse numbers, reserve retired fields, and create new package versions for incompatible redesigns. Automated checks protect the contract between every team.