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
| Change | Problem |
|---|---|
| Change a field number | Readers decode data into the wrong field |
| Change a field type | Wire formats differ and decoding fails or corrupts data |
| Reuse a deleted number | Old data lands in a new field |
| Remove an RPC method | Old clients receive UNIMPLEMENTED errors |
| Change a method request type | Messages 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.
- Update the proto file with the new field.
- Deploy servers that understand the new field.
- Deploy clients that start sending the new field.
- 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.
