Skip to content

Style guide#

Buf recommends a set of Protobuf naming and layout conventions that keep schemas readable and evolvable across teams. Every rule on this page comes from the STANDARD lint category, which buf lint enforces when configured. Turn the whole set on with one block in your buf.yaml:

buf.yaml
version: v2
lint:
  use:
    - STANDARD

Each check below links back to the rule reference via (Why?) for the rationale.

Files and packages#

All files should have a package defined. (Why?)

All files of the same package should be in the same directory, and every file should live in a directory that matches its package name. (Why?)

For example, with this module defined in the proto directory, expect these package values:

.
├── buf.yaml
└── proto
    └── foo
        └── bar
            ├── bat
            │   └── v1
            │       └── bat.proto // package foo.bar.bat.v1
            └── baz
                └── v1
                    ├── baz.proto         // package foo.bar.baz.v1
                    └── baz_service.proto // package foo.bar.baz.v1

Packages should be in lower_snake_case format. (Why?)

The last component of a package should be a version. (Why?)

Filenames should be in lower_snake_case.proto format. (Why?)

All of the file options below should have the same value, or all be unset, across every file that shares a package: (Why?)

  • csharp_namespace
  • go_package
  • java_multiple_files
  • java_package
  • php_namespace
  • ruby_package
  • swift_prefix

For example, given a file foo_one.proto:

foo_one.proto
syntax = "proto3";

package foo.v1;

option go_package = "foov1";
option java_multiple_files = true;
option java_package = "com.foo.v1";

Another file foo_two.proto with package foo.v1 must have those three options set to the same values, with the other options unset:

foo_two.proto
syntax = "proto3";

package foo.v1;

option go_package = "foov1";
option java_multiple_files = true;
option java_package = "com.foo.v1";

Imports#

Don’t declare imports as public or weak. (Why?)

Enums#

Enums shouldn’t have the allow_alias option set. (Why?)

Enum names should be PascalCase. (Why?)

Enum value names should be UPPER_SNAKE_CASE. (Why?)

Prefix enum value names with the UPPER_SNAKE_CASE of the enum name. (Why?) For example, given the enum FooBar, prefix all enum value names with FOO_BAR_.

Suffix the zero value for all enums with _UNSPECIFIED. (Why?) For example, given the enum FooBar, the zero value should be FOO_BAR_UNSPECIFIED = 0;. Configure an alternative suffix in your lint configuration.

Messages#

Message names should be PascalCase. (Why?)

Field names should be lower_snake_case. (Why?)

Oneof names should be lower_snake_case. (Why?)

Services and RPCs#

Service names should be PascalCase. (Why?)

Suffix service names with Service. (Why?) Configure an alternative suffix in your lint configuration.

RPC names should be PascalCase. (Why?)

Every RPC should have its own request and response messages, not shared with any other RPC. (Why?)

Name RPC request and response messages after the RPC, either as MethodNameRequest and MethodNameResponse, or as ServiceNameMethodNameRequest and ServiceNameMethodNameResponse. (Why?)

Use of the Empty well-known type as a request and response type#

Giving each RPC its own request and response message preserves maximum flexibility to evolve the RPC without breaking backward compatibility. This applies to Protobuf in general, for both gRPC and ConnectRPC.

Don’t import and use Empty when an RPC doesn’t happen to have any request or response data yet. Define a custom empty request and/or response message per RPC instead. When the request or response eventually gains fields, you can add them without fear of breaking changes.

If you’re modeling a message that’s genuinely always empty (as opposed to one whose shape you’re not yet sure about), Empty is fine. Custom empty request and response types are more future-proof.

To use Empty with buf lint, set the corresponding flags in buf.yaml to avoid lint errors:

buf.yaml with flags set to allow Empty
version: v2
lint:
  use:
    - STANDARD
  rpc_allow_google_protobuf_empty_requests: true
  rpc_allow_google_protobuf_empty_responses: true

Comments and file layout#

Use // instead of /* */ for comments.

Over-document, and use complete sentences in comments. Put documentation above the type, not inline.

Lay files out in this order, matching Google’s current recommendations. buf format enforces everything below except the first two items:

  • License header (if applicable)
  • File overview
  • Syntax
  • Package
  • Imports (sorted)
  • File options
  • Everything else

Design recommendations#

These aren’t strictly style rules, but Buf recommends them for any production Protobuf schema.

Set up breaking change detection from day one. See breaking change detection for how Buf enforces it.

Avoid keywords from popular languages in any type or package name. A package named foo.internal.bar blocks importing the generated Go stubs from other Go packages, because internal is a reserved directory name in Go.

Use pluralized names for repeated fields.

Name fields after their type when possible. For a field of message type FooBar, name the field foo_bar unless there’s a specific reason to do otherwise.

Avoid nested enums and nested messages. You may want to reference them outside their enclosing message later, even if you don’t think so now.

Avoid streaming RPCs. They’re difficult to implement and call, and they often require special configuration in proxies, firewall rules, and other network infrastructure. Polling and pagination are usually much simpler and nearly as efficient. For the handful of cases where streaming RPCs are worth the complexity, add exceptions to your lint configuration.

See also#

  • Lint rules: The authoritative list of every rule referenced above, with configuration details.
  • buf format: Auto-formats file layout to match the order in this guide.
  • Breaking change detection: The companion check to lint, for schema evolution over time.