Recommendation: Keep directory-based versioning (v1/, v2/) in addition to Buf versioning.
While Buf provides powerful versioning capabilities through the Buf Schema Registry (BSR), directory-based versioning serves a different and complementary purpose. Both are necessary for a complete enterprise API management strategy.
This document evaluates whether directory-based proto versioning (proto/core/v1/, proto/core/v2/) is necessary given Buf's built-in versioning strategies and policies.
Purpose: API evolution and breaking change management
Example:
proto/
├── core/v1/ # Stable, never changes
│ └── user.proto # package core.v1;
└── core/v2/ # New version with breaking changes
└── user.proto # package core.v2;
What it provides:
- Multiple API versions can coexist simultaneously
- Clients can choose which version to use
- Breaking changes don't affect existing clients
- Clear migration path between versions
- Package names in code reflect versions (e.g.,
core.v1vscore.v2)
Purpose: Module publication and dependency management
What Buf provides:
- BSR (Buf Schema Registry): Version control for published proto modules
- Semantic Versioning: Tracks changes using SemVer (1.0.0, 1.1.0, 2.0.0)
- Breaking Change Detection: Automatically detects backward-incompatible changes
- Dependency Management: Manages dependencies between proto modules
- Build Reproducibility: Lock files ensure consistent builds
Example (buf.yaml):
version: v2
modules:
- path: proto
name: buf.build/geniustechspace/api-contracts| Aspect | Directory-Based (v1/, v2/) | Buf Versioning (BSR) |
|---|---|---|
| Scope | Individual API versions | Entire module releases |
| Granularity | Per service/package | Per repository/module |
| Purpose | API evolution | Dependency management |
| Coexistence | Multiple versions at runtime | Single version in codebase |
| Client Impact | Clients choose version | Clients pin dependency version |
| Breaking Changes | New directory (v2) | New major version (2.0.0) |
| Use Case | Long-term API compatibility | Build reproducibility |
Directory-based versioning allows multiple API versions to exist simultaneously in the same codebase and run in the same service:
// Both versions available at runtime
import corev1 "github.com/example/api/core/v1"
import corev2 "github.com/example/api/core/v2"
// Service implements both
type Server struct {
corev1.UnimplementedUserServiceServer
corev2.UnimplementedUserServiceServer
}Buf versioning cannot provide this - you get one version per build.
With directory versioning:
- Old clients continue using v1 indefinitely
- New clients adopt v2 at their own pace
- No forced upgrades or breaking changes
- Services support both versions during migration period
Example migration timeline:
Year 1: Launch v1
Year 2: Launch v2, both v1 and v2 supported
Year 3-5: Gradual client migration from v1 to v2
Year 6: Deprecate v1 (with advance notice)
Directory structure provides clear package names in generated code:
// proto/core/v1/user.proto
package core.v1;
// proto/core/v2/user.proto
package core.v2;Generated code:
use api_contracts::core::v1::User as UserV1;
use api_contracts::core::v2::User as UserV2;This makes version explicit in code and prevents accidental mixing of versions.
Directory-based versioning is the recommended approach in the Protocol Buffers ecosystem:
- Google APIs: Uses directory versioning extensively (google/cloud/vision/v1, google/cloud/vision/v2)
- gRPC: Recommends package versioning for API evolution
- Industry Standard: Most major gRPC APIs use this pattern
- Buf Recommendation: Buf docs recommend combining both approaches
When you need breaking changes:
Without directory versioning:
❌ Problem: Must force all clients to upgrade
❌ Risk: Client code breaks
❌ Process: Coordination nightmare
With directory versioning:
✅ Solution: Create v2 alongside v1
✅ Safety: No client breaks
✅ Process: Smooth migration
Buf versioning handles:
-
Module Publishing: Publishing versioned releases to BSR
buf push --tag v1.2.3
-
Dependency Management: Other modules depend on specific versions
deps: - buf.build/geniustechspace/api-contracts:v1.2.3
-
Breaking Change Detection: CI fails on accidental breaking changes
buf breaking --against '.git#tag=v1.2.0' -
Reproducible Builds: Lock file ensures same dependencies
# buf.lock version: v2 deps: - name: buf.build/googleapis/googleapis commit: abc123...
Use when:
- ✅ Making breaking changes to existing APIs
- ✅ Need to support multiple API versions simultaneously
- ✅ Clients need time to migrate between versions
- ✅ Creating major new API surfaces
Process:
- Start with v1:
proto/service/v1/ - Maintain v1 as stable (only non-breaking additions)
- When breaking changes needed: Create v2:
proto/service/v2/ - Support both v1 and v2 in parallel
- Eventually deprecate v1 (with long notice period)
Use for:
- ✅ Publishing releases to BSR
- ✅ Dependency management between modules
- ✅ CI/CD versioning and tagging
- ✅ Client library versioning
Process:
- Tag releases:
v1.0.0,v1.1.0,v2.0.0 - Push to BSR:
buf push --tag v1.1.0 - Clients depend on specific versions
- SemVer indicates compatibility:
- Patch (1.0.1): Bug fixes only
- Minor (1.1.0): New features, backward compatible
- Major (2.0.0): Breaking changes (likely includes new v2 directory)
Repository State Buf Version Notes
─────────────────────────────────────────────────────────────
proto/core/v1/ v1.0.0 Initial release
proto/core/v1/ v1.1.0 Added new fields (non-breaking)
proto/core/v1/ v1.2.0 Added new service (non-breaking)
proto/core/v1/ v1.2.1 Bug fix in docs
proto/core/v1/ + v2/ v2.0.0 Breaking changes → new v2 directory
proto/core/v1/ + v2/ v2.1.0 Added features to v2
proto/core/v1/ + v2/ v2.1.1 Bug fix in v2
Step 1: Create v2 directory
mkdir -p proto/core/v2
cp proto/core/v1/user.proto proto/core/v2/user.protoStep 2: Make breaking changes in v2
// proto/core/v2/user.proto
package core.v2;
message User {
string id = 1;
string full_name = 2; // CHANGED: was first_name + last_name
string email = 3;
// Breaking: removed deprecated fields
}Step 3: Keep v1 stable
// proto/core/v1/user.proto (unchanged)
package core.v1;
message User {
string id = 1;
string first_name = 2;
string last_name = 3;
string email = 4;
}Step 4: Update service to support both
// Service implements both versions
type UserService struct {
v1.UnimplementedUserServiceServer
v2.UnimplementedUserServiceServer
}Step 5: Tag as major version
git tag v2.0.0
buf push --tag v2.0.0Our current configuration correctly enforces versioning:
lint:
use:
- PACKAGE_VERSION_SUFFIX # Enforces v1, v2 in package names
- PACKAGE_DIRECTORY_MATCH # Package must match directoryThis ensures:
- Package names must end with version suffix (v1, v2)
- Directory structure must match package names
- Consistency across entire codebase
Reality: They serve different purposes. Buf versions the module release, directory versions the API contract.
Reality: It's essential for supporting multiple API versions simultaneously in production.
Reality: buf breaking prevents accidental breaking changes. Sometimes you need breaking changes - that's when you create v2.
Reality: It's also about forward evolution - letting you experiment with v2 while v1 remains stable.
- Use directory versioning (v1/, v2/) for API evolution
- Use Buf versioning (SemVer) for module releases
- Never break v1 - make v2 instead
- Support multiple versions during migration periods
- Deprecate old versions with plenty of notice (6-12 months)
- Document version differences clearly
- Use buf breaking to prevent accidental breaking changes in same version
- Don't make breaking changes to existing versions
- Don't force immediate migration - give clients time
- Don't create v2 prematurely - only when truly needed
- Don't support versions forever - have sunset plans
- Don't mix versions in the same package name
- Don't rely only on Buf versioning for API evolution
Keep directory-based versioning (v1/, v2/). It is not redundant - it serves a fundamentally different purpose than Buf versioning:
- Directory versioning: API contract evolution (v1 vs v2 APIs)
- Buf versioning: Module release management (v1.2.3 vs v1.2.4)
Both are necessary for a robust enterprise API strategy. They complement each other:
- Buf ensures safe, reproducible builds and dependency management
- Directory versioning ensures smooth API evolution without breaking clients
This combination provides:
- ✅ Backward compatibility through parallel version support
- ✅ Clear migration paths for breaking changes
- ✅ Industry-standard API evolution patterns
- ✅ Safe dependency management via Buf
- ✅ Flexibility to evolve APIs without disruption