The buf tool is the definitive way to manage Protobuf definitions, and its most surprising feature is that it doesn’t treat your .proto files as just language-agnostic data structures.

Let’s see buf in action. Imagine you have a simple Protobuf message for a user:

// proto/user.proto
syntax = "proto3";

package myapp.v1;

message User {
  string id = 1;
  string name = 2;
}

And another for a related entity, like a Profile:

// proto/profile.proto
syntax = "proto3";

package myapp.v1;

import "proto/user.proto"; // Importing user.proto

message Profile {
  User user = 1;
  string bio = 2;
}

Traditionally, you’d just run protoc to generate code for different languages. But buf adds a crucial layer of management. First, you need a buf.yaml file in your project’s root to define your module:

# buf.yaml
version: v1
name: buf.build/yourusername/yourrepo
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

This buf.yaml tells buf that your Protobuf definitions live in buf.build/yourusername/yourrepo. The breaking and lint sections specify rules for checking compatibility and style. Now, you can use buf to perform powerful operations.

To generate code, you’d typically use a buf.gen.yaml file:

# buf.gen.yaml
version: v1
plugins:
  - plugin: buf.build/gen/go
    out: gen/go
  - plugin: buf.build/gen/python
    out: gen/python

And then run:

buf generate

This command, guided by buf.yaml and buf.gen.yaml, will fetch your Protobuf definitions and any dependencies (if you were using external Protobufs), apply linting and breaking change checks, and then generate code for Go and Python into the gen/go and gen/python directories respectively.

The real power of buf comes from its understanding of Protobuf modules and dependencies. Unlike protoc which just sees a flat directory of .proto files, buf treats your Protobuf definitions as a versioned artifact that can be published and consumed. This is where the "modern Protobuf workflow" really shines.

You can publish your Protobuf modules to a registry (like Buf’s own registry, or a private one) and then depend on them in other projects. For example, if you had a user.proto definition in a separate module buf.build/yourusername/common-protos, your buf.yaml would look like this:

# buf.yaml (in a different project depending on common-protos)
version: v1
name: buf.build/yourusername/my-app
deps:
  - buf.build/yourusername/common-protos:v1.2.0 # Specific version dependency
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

When you run buf generate in this dependent project, buf will automatically fetch buf.build/yourusername/common-protos version v1.2.0, check for breaking changes against your local definitions, and then generate code for both your local protos and the fetched dependencies. This ensures API compatibility and provides a robust dependency management system for your Protobuf interfaces.

The buf tool enforces a structured approach to Protobuf development, treating your .proto files not just as schema definitions but as versioned, publishable API contracts. This shift in perspective is what enables true dependency management, robust breaking change detection, and a scalable workflow across multiple services and teams.

The next logical step is to explore how buf integrates with CI/CD pipelines to automate these checks and generation steps.

Want structured learning?

Take the full Protobuf course →