← Writing

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:

  1. License header (if applicable)

  2. File overview

  3. Syntax

  4. Package

  5. Imports (sorted)

  6. File options

  7. Service (1 service per file)

  8. Messages (sorted in the same order of the RPCs and grouped under a comment// region $RPCNAME)

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

PROTOBUF
message SongServerRequest {
  optional string song_name = 1;
}

Using this naming convention for field names gives you accessors like the following:

PROTOBUF
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:

PROTOBUF
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:

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

PROTOBUF
  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

PROTOBUF
// 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

PROTOBUF
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:

PROTOBUF
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:

PROTOBUF
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:

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

PROTOBUF
// 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.

PROTOBUF
// 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:

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.

PROTOBUF
// 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.

PROTOBUF
// 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:

YAML
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

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.

References

GOOD THINGS START WITH A CONVERSATION.

Something on your mind?

LinkedIn
sonny@the-internet: ~

A small window into a curious mind.

Type help to look around. No serious hacking required.

↑ ↓ command historytry a little curiosity