| name | proto-design |
| description | Protocol Buffers API design — message structure, field numbering, backward compatibility, enums, well-known types, and oneof. Use when designing or evolving proto schemas. |
Protocol Buffers API Design
Message Design Principles
Single Responsibility
Each message should represent one concept. Avoid mega-messages that combine unrelated data.
// GOOD: focused messages
message CreateUserRequest {
string display_name = 1;
string email = 2;
}
// BAD: kitchen-sink message
message UserOperation {
string name = 1;
string email = 2;
bool is_delete = 3;
string search_query = 4;
}
Request/Response Naming
Follow the pattern <MethodName>Request and <MethodName>Response:
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
}
Field Numbering
Encoding Cost
- Fields 1-15: 1 byte tag (use for frequently set fields)
- Fields 16-2047: 2 byte tag
- Fields 2048-262143: 3 byte tag
Rules
- Never reuse a field number after removing a field
- Never change the type of an existing field
- Use
reserved to prevent reuse of removed fields
message User {
reserved 3, 8; // removed field numbers
reserved "old_field_name"; // removed field names
string name = 1;
string email = 2;
// field 3 was removed
int32 age = 4;
}
Backward & Forward Compatibility
Safe Changes (non-breaking)
- Add new fields (with new field numbers)
- Add new enum values
- Add new RPC methods to a service
- Add new services to a proto file
- Remove fields (mark as
reserved)
- Rename fields (wire format uses numbers, not names)
Breaking Changes (avoid)
- Change a field number
- Change a field type
- Remove or reorder enum values
- Rename a service or package
- Change the request/response type of an RPC
Enums
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0; // always required
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
Rules
- Zero value must be
_UNSPECIFIED — it is the default when the field is not set
- Prefix all values with the enum name in UPPER_SNAKE_CASE
- Never remove enum values — add new ones at the end
- Use
allow_alias = true only when genuinely needed
Oneof
Use oneof for mutually exclusive fields:
message SearchFilter {
oneof filter {
string user_id = 1;
string email = 2;
string phone_number = 3;
}
}
Rules
- Oneof fields share memory — only one can be set
- Setting a oneof field clears all others
- You cannot use
repeated inside a oneof
- Name the oneof with lower_snake_case
Well-Known Types
Import from google/protobuf/:
| Type | Use For | Import |
|---|
Timestamp | Points in time | google/protobuf/timestamp.proto |
Duration | Time spans | google/protobuf/duration.proto |
Empty | No-payload RPCs | google/protobuf/empty.proto |
Any | Polymorphic messages | google/protobuf/any.proto |
FieldMask | Partial updates | google/protobuf/field_mask.proto |
StringValue | Nullable string | google/protobuf/wrappers.proto |
Int32Value | Nullable int32 | google/protobuf/wrappers.proto |
BoolValue | Nullable bool | google/protobuf/wrappers.proto |
Struct | Dynamic JSON-like data | google/protobuf/struct.proto |
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
message Event {
string name = 1;
google.protobuf.Timestamp created_at = 2;
}
service EventService {
rpc DeleteEvent (DeleteEventRequest) returns (google.protobuf.Empty);
}
Pagination Pattern
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
Error Detail Pattern
Use google.rpc.Status with detail Any messages for rich error responses:
import "google/protobuf/any.proto";
message ErrorDetail {
string field = 1;
string description = 2;
}
API Versioning Strategy
Package-Based Versioning
Use the proto package to version APIs. Each major version gets its own package and directory:
Protos/
v1/
user_service.proto
order_service.proto
v2/
user_service.proto
// Protos/v1/user_service.proto
syntax = "proto3";
package myapp.v1;
option csharp_namespace = "MyApp.V1";
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
}
// Protos/v2/user_service.proto
syntax = "proto3";
package myapp.v2;
option csharp_namespace = "MyApp.V2";
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
rpc SearchUsers (SearchUsersRequest) returns (SearchUsersResponse); // New in v2
}
Running Multiple Versions Simultaneously
Register both versions in Program.cs:
app.MapGrpcService<V1.UserServiceImpl>();
app.MapGrpcService<V2.UserServiceImpl>();
Backward-Compatible Changes (No Version Bump)
These changes are safe within the same version:
- Adding new fields to existing messages (new field number)
- Adding new RPC methods to a service
- Adding new enum values (except changing the default)
- Adding new messages
- Changing
optional to repeated (wire-compatible for scalar types)
Breaking Changes (Require New Version)
These changes require a new major version:
- Removing or renaming an RPC method
- Removing or renaming a message field
- Changing a field's type or number
- Changing a service name
- Removing an enum value
- Changing the response type of an RPC
Deprecating Old RPCs
service UserService {
// Deprecated: Use SearchUsers instead.
rpc FindUsers (FindUsersRequest) returns (FindUsersResponse) {
option deprecated = true;
};
rpc SearchUsers (SearchUsersRequest) returns (SearchUsersResponse);
}
Version Lifecycle
| Phase | Action |
|---|
Alpha (v1alpha1) | Unstable, may change without notice |
Beta (v1beta1) | Feature-complete, may have minor changes |
Stable (v1) | Breaking changes only in new major version |
Deprecated (v1 with deprecated) | Announce sunset, maintain for migration window |
| Sunset | Remove after migration period |
Breaking Change Detection with Buf
version: v2
breaking:
use:
- FILE
buf breaking --against '.git#branch=main'
Versioning Checklist
| Rule | Why |
|---|
Use package-based versioning (myapp.v1) | Clear separation, multiple versions coexist |
| Never reuse field numbers | Binary incompatibility |
Add option deprecated = true before removing RPCs | Clients get compiler warnings |
Use reserved for removed field numbers and names | Prevents accidental reuse |
Run buf breaking in CI | Catches accidental breaking changes |
Set csharp_namespace per version | Prevents C# namespace conflicts |
| Maintain a migration guide between versions | Helps clients upgrade |