109. Case Study: Managing Protobuf Dependencies with Buf for Cross-platform
title: “109. Case Study: Managing Protobuf Dependencies with Buf for Cross-platform” published: true author: Senior Software Engineer tags:
- protobuf
- buf
- cross-platform
- dependency management
- grpc
Dependency management for Protocol Buffers (protobuf) is often a classic problem when developing systems made up of many services or different platforms; whether those are microservices, frontend-backend setups, or even cross-language scenarios such as Go, Java, Python, and JavaScript. In this article, I will walk through a case study of managing Protobuf dependencies using Buf, a modern tool that is increasingly becoming the new standard, while also sharing the shape of the problem, the technical solution, and best practices you can apply if you find yourself in a similar landscape.
Background of the Problem
Let’s start with a fairly familiar picture:
- There are many repos (monorepo or multi-repo) that use
.protofiles to define APIs (gRPC, REST). - Every project and team wants the freedom to pick its own language: Go, Node.js, Python, Java, and so on.
- Copy-pasting proto files becomes routine; versioning is often manual and easy to miss.
- You want clean code, automation, and good scaling without breaking changes.
The fundamental problems:
- Stale dependencies: Custom proto files are often outdated across repos.
- Conflicting versions: Duplicate proto definitions with different versions.
- Difficulty in updating: Hard to update the schema without the risk of incompatibility.
- Multi-language generation: Stub code from proto must be generated for many languages, and dependencies must stay consistent.
Why Buf?
Buf is a toolkit for linting, building, breaking change analysis, artifact generation, and – most importantly – dependency management plus a module registry for Protobuf.
Key benefits of Buf:
- Version and distribute Protobuf schemas through a registry.
- Manage proto package dependencies the same way npm does for JavaScript.
- Easy to enforce rules and linting.
- Reproducible builds across many languages.
Case Study: Multi-platform Payment System
Assume we have a payment system like the following:
- A payment service (Go)
- A notification service (Node.js)
- Mobile apps (Flutter/Dart)
- A web frontend (React/TypeScript)
All of them consume the transaction.proto and user.proto schemas.
The Classic Problem
Without dependency management, the workflow usually looks like this:
┌──────────────────┐
│ payment-service │
└───────┬──────────┘
│
┌───────▼──────────┐
│ notification │
└───────┬──────────┘
│
┌───────▼───────────────┐
│ mobile & web clients │
└───────────────────────┘
Each team copies the .proto files into their own repos. In the end, every repo can have a different version of the proto file, with uncontrolled differences.
The Architectural Solution with Buf
Let’s look at an architecture diagram of proto dependency management orchestrated by Buf:
flowchart TD
subgraph Buf Registry
A[transaction/v1]
B[user/v1]
end
subgraph Services & Client
S1[payment-service (Go)]
S2[notification (Node.js)]
S3[mobile-app (Dart)]
S4[web-app (TypeScript)]
end
S1-->|import@buf.build/org/transaction|A
S2-->|import@buf.build/org/transaction|A
S1-->|import@buf.build/org/user|B
S3-->|import@buf.build/org/transaction|A
S4-->|import@buf.build/org/transaction|A
S2-->|import@buf.build/org/user|B
S4-->|import@buf.build/org/user|B
Now, all services and client apps pull their dependencies from the Buf Registry. No copy-pasting, no invisible divergence.
Implementation Using Buf
1. Protobuf Module Directory Structure
For example, suppose we have a dedicated repo for proto schemas:
1proto/
2 ├── buf.yaml
3 ├── buf.lock
4 └── transaction/
5 └── v1/
6 └── transaction.prototransaction/v1/transaction.proto
1syntax = "proto3";
2
3package transaction.v1;
4
5message Transaction {
6 string id = 1;
7 string user_id = 2;
8 double amount = 3;
9 string status = 4;
10}2. Module Configuration in Buf
buf.yaml
1version: v1
2name: buf.build/yourorg/transaction
3lint:
4 use:
5 - DEFAULT
6breaking:
7 use:
8 - FILEbuf.lock will be generated automatically.
3. Publishing to the Buf Registry
Buf provides a CLI command to publish a module:
1$ buf pushbuf.build/yourorg/transaction can be imported anywhere.4. Consuming Proto via Buf in a Service
For example, inside the Go payment microservice repo:
buf.yaml
1version: v1
2deps:
3 - buf.build/yourorg/transactionBuf.lock acts like a generator similar to node_modules in NPM:
it locks dependencies so versions stay consistent.
5. Generate Code for Any Language
With Buf, multi-language generation is cleaner than with vanilla protobuf.
Example of generating Go code:
1$ buf generate --template buf.gen.yamlbuf.gen.yaml
1version: v1
2plugins:
3 - plugin: go
4 out: gen/go
5 - plugin: go-grpc
6 out: gen/goFor Node.js, Dart, or TypeScript, you only need to add the relevant plugin.
Simulating a Dependency Update
Suppose we want to update the Transaction schema by adding a created_at field. The update workflow:
- Update
transaction.proto - Push to the Buf Registry (
buf push) - Update the dependencies in each service (
buf mod update) - Regenerate the code stubs (buf generate)
All consumers pull the latest version with buf mod update.
Comparison Table: Buf vs Manual Copy
| Feature | Manual Copy | Buf Registry |
|---|---|---|
| Versioning | Manual | Automatic |
| Dependency Lock | None | Yes (buf.lock) |
| Lint & Breaking Check | Manual | Built-in |
| Multi-language Generator | Complicated | Simple |
| Up-to-date Dependencies | Often Wrong | Consistent |
| CI/CD Integration | Complicated | Easy |
Best Practices & Lessons Learned
- One source of truth: There is only one Protobuf repo published to the Buf Registry.
- Avoid breaking changes: Buf’s breaking check is incredibly helpful for backward compatibility.
- Automate in CI/CD: Linting, checking, and regeneration run automatically on every proto commit.
- Buf lock (buf.lock): Dependencies that are pinned and reproducible.
- Documented registry versioning: Every team knows which version and semver its dependency pulls from.
Conclusion
The experience of managing cross-platform Protobuf dependencies is easier, more scalable, and far less error-prone with Buf Module & Registry than with copy-paste techniques or other custom versioning solutions. With repeatable infrastructure as code, an engineering team can save time, reduce human error, and collaborate more effectively across languages and platforms.
Buf is not merely a linter; it is a new way of thinking about Protobuf schema and dependency management—and from my experience, the upfront investment in Buf will save you a great deal of debugging and refactoring time in the long run.
References
If you have a unique experience managing Protobuf dependencies, share it in the comments!