Revised article
gRPC proto files Best Practices
This article is a mash-up of different sources cited in references and Pixelmatic guidelines aimed to serve as coding and style guides for proto files in the context of gRPC services definitions.
We used these guidelines at Pixelmatic for the game Infinite Fleet: the article lived in the repository, and later the CI checked the proto files with buf.
Some rules below are team conventions, not requirements of the format.
Coding and style guides
In short, the philosophy of the guides is:
-
Be consistent
-
Reuse the well-known types from Google
-
But keep separation as needed to avoid breaking changes
Standard file formatting
-
Keep the line length to 80 characters.
-
Prefer the use of double quotes for strings.
File structure
Files should be named lower_snake_case.proto
All files should be ordered in the following manner:
-
License header (if applicable)
-
File overview
-
Syntax
-
Package
-
Imports (sorted)
-
File options
-
Service (1 service per file)
-
Messages (sorted in the same order of the RPCs and grouped under a comment
// region $RPCNAME)
service Foo {
rpc Bar(BarRequest) returns (BarResponse);
}
// region Bar
message BarRequest {
// ...
}
message BarResponse {
// ...
}
Note: // region comments are collapsible in Visual Code.
Packages
Package names should be in lowercase. Package names should have unique names based on the project name and possibly based on the path of the file containing the protocol buffer type definitions.
Message and field names
Use CamelCase (with an initial capital) for message names – for example, SongServerRequest. Use underscore_separated_names for field names (including oneof field and extension names) – for example, song_name.
message SongServerRequest {
optional string song_name = 1;
}
Using this naming convention for field names gives you accessors like the following:
C++:
const string& song_name() { ... }
void set_song_name(const string& x) { ... }
Java:
public String getSongName() { ... }
public Builder setSongName(String v) { ... }
If your field name contains a number, the number should appear after the letter instead of after the underscore. For example, use song_name1 instead of song_name_1
Optional fields
Consider the following proto3 message which defines the field bar:
message Foo {
int32 bar = 1;
}
With this definition, it is impossible to check whether bar has been set to 0 or if no value has been set since the default value of int32 fields is 0.
To allow you to make the difference, use:
message Foo {
optional int32 bar = 1;
}
This exposes hasBar() and clearBar() (depending on the language) methods in the generated code.
Repeated fields
Use pluralized names for repeated fields.
repeated string keys = 1;
...
repeated MyMessage accounts = 17;
Oneof
Oneof is a wonderful example where a protobuf language feature helps to make gRPC APIs more intuitive. As an example, imagine we have a service method where users are able to change their profile picture, either from an URL or by uploading their own (small) image. Instead of doing this
// Either set image_url or image_data. Setting both will result in an error.
message ChangeProfilePictureRequest {
string image_url = 1;
bytes image_data = 2;
}
we can define the desired behavior directly into the message with oneof
message ChangeProfilePictureRequest {
oneof image {
string url = 1;
bytes data = 2;
}
}
Not only is that much clearer for API consumers, but it is also easier to check which field has been set in the generated code. Keep in mind that oneof also allows that none of the fields has been set, meaning there is no need to introduce a separate none field if the oneof should be optional.
Flexible Data
If you do not know in advance the nature of the data you are going to receive from your consumer, we suggest you use the well know type Struct. For example:
message StructTest {
google.protobuf.Struct data = 1;
}
Enums
Use CamelCase (with an initial capital) for enum type names and CAPITALS_WITH_UNDERSCORES for value names:
enum FooBar {
FOO_BAR_UNSPECIFIED = 0;
FOO_BAR_FIRST_VALUE = 1;
FOO_BAR_SECOND_VALUE = 2;
}
-
Each enum value should end with a semicolon, not a comma. Prefer prefixing enum values instead of surrounding them in an enclosing message. The zero value enum should have the suffix
UNSPECIFIED. -
Note that names of enum entries must be unique in the whole package. Defining a second completely unrelated enum with an entry existing in the first will result in an error message because of how enums in C and C++ are implemented. To avoid this, prefix the enum entries with the enum name. Some code generators (ex. for C#) will remove these prefixes automatically so that the resulting code looks "clean" again.
Services
If your .proto defines an RPC service, you should use CamelCase (with an initial capital) for both the service name and any RPC method names:
service FooService {
rpc GetSomething(GetSomethingRequest) returns (GetSomethingResponse);
rpc ListSomething(ListSomethingRequest) returns (ListSomethingResponse);
}
Separate request and response messages
We recommend that you create a separate message for each request and response. Name them {MethodName}Request and {MethodName}Response. This allows you to modify request and response messages for a single service method without introducing accidental changes to other methods. It is tempting to re-use messages and simply ignore fields that aren't needed. With time, this will result in a mess, since it isn't obvious what the API expects. Exceptions to this rule are usually made when returning a single, well-defined entity or when returning an empty message.
// Service fragment; request and response messages are defined separately.
service BookService {
rpc CreateBook(CreateBookRequest) returns (CreateBookResponse);
rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);
rpc GetBook(GetBookRequest) returns (GetBookResponse);
rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty);
}
Use the error system
Do not return a simple confirmation to an endpoint that does not need to return any other data, use google.protobuf.Empty and trigger an error if needed.
// Don't
service BookService {
rpc DeleteBook(DeleteBookRequest) returns (DeleteBookResponse);
}
message DeleteBookResponse {
bool deleted = 1;
}
// Do
service BookService {
rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty);
}
Don’t reinvent the wheel
Use and abuse of the well-known types:
Read on developers.google.com ↗
Examples:
-
The empty message is already defined as
google.protobuf.Empty, so it doesn't make sense to define yet another empty message. -
Don’t try to redefine timestamp, there’s an existing type for it: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#timestamp
-
...
Don’t have one field in a struct influence the meaning of another
Prefer a schema that makes the meaning of a field explicit. When two values are mutually exclusive in the domain, oneof can express that relationship. Created and updated timestamps are not inherently mutually exclusive: many records need both.
// don't do this!
message Foo {
int64 timestamp = 1;
bool timestamp_is_created = 2; // true: created time; false: updated time
}
This causes confusion — the client now needs to have special logic to know how to interpret one field based on another. Instead, use multiple fields, or use the protobuf “oneof” feature.
// Complete file: alternatives for two different domain requirements.
syntax = "proto3";
package examples;
import "google/protobuf/timestamp.proto";
// Use independent fields if both timestamps may legitimately exist.
message RecordTimes {
google.protobuf.Timestamp created = 1;
google.protobuf.Timestamp updated = 2;
}
// Use oneof if the domain represents exactly one kind of event.
// oneof also permits neither field; validate presence in the application.
message RecordEvent {
oneof event {
google.protobuf.Timestamp created = 1;
google.protobuf.Timestamp updated = 2;
}
}
Linting
We use protolint to lint the proto files. Please download and install protolint from https://github.com/yoheimuta/protolint/releases.
Git pre-commit hook
You can also enforce protolint to execute automatically before committing code using pre-commit. Install pre-commit, add a file .pre-commit-config.yaml, and add this content to it:
repos:
- repo: https://github.com/yoheimuta/protolint
rev: REPLACE_WITH_A_REVIEWED_RELEASE_TAG
hooks:
- id: protolint
Replace REPLACE_WITH_A_REVIEWED_RELEASE_TAG with a release you have reviewed before running pre-commit install. Pin the tool version rather than following a moving branch.
Extra recommendations
-
Use CI to automatically double-check if your proto files still compile without error/warnings.
-
Avoid large messages. gRPC is not designed for that. Details: https://kreya.app/blog/grpc-best-practices/#large-messages
-
Reuse channels. Creating a gRPC channel is a costly process, as it creates a new HTTP/2 connection. Details: https://kreya.app/blog/grpc-best-practices/#reuse-channels
Evolving a published schema
Never reuse a field number after deleting a field: reserve its number and name. And check compatibility with existing clients before changing a type or moving a field into a oneof (buf can check breaking changes for you).
2026 update
Some of the rules were a bit custom. Today, for simplicity, I would rely more on buf's native rules. When I started this article, I had not discovered buf yet.