MVS Dependency Resolution
Architecture deep-dive into Minimal Version Selection (MVS), diamond dependency graph resolution, protobuf.mod specification, and hermetic lockfiles in EasyP.
EasyP adopts Minimal Version Selection (MVS) as its core dependency resolution algorithm for Protocol Buffer modules. Designed by Russ Cox for Go modules, MVS departs from traditional package managers (such as Cargo or npm) by selecting the oldest (minimal) explicit version that satisfies all module constraints rather than attempting complex constraint satisfaction or defaulting to the latest version.
This document provides a technical deep-dive into the MVS algorithm, its diamond dependency resolution behavior, the protobuf.mod domain-specific language (DSL), the hermetic protobuf.lock file format, and a comparison with alternative dependency models.
1. Minimal Version Selection (MVS) Principles
Traditional dependency resolution algorithms (such as Cargo's PubGrub or npm's Max Version strategy) treat version selection as a Constraint Satisfaction Problem (CSP) solved via NP-complete SAT solvers or aggressive "latest version" upgrades. In contrast, EasyP's implementation of MVS is built around predictability, determinism, and independence from global registry state.
Key Motivations for MVS in EasyP
| Challenge in Proto Management | Traditional / Buf v2 Approach | EasyP MVS Approach |
|---|---|---|
| Diamond Dependency Conflicts | Global workspace-wide resolution forces a single version across all consumers, causing cross-team coupling. | Per-module independent resolution graph. Each module resolves its own minimal requirements. |
| Registry Volatility | latest resolution or registry updates can silently alter build output over time. | Deterministic resolution selecting max(requested_minimums). No SAT solver or registry lookups. |
| Proto Schema Collision | Multiple major/minor versions compiled in one proto-root break Fully Qualified Names (FQNs). | Exactly one version of each dependency per module graph, strictly locked via protobuf.lock. |
Core Algorithm Behavior
- Explicit Version Bounds: Every requirement in
protobuf.modspecifies an explicit baseline version (e.g.,github.com/acme/weather v1.2.0). This version is treated as a minimum requirement, not a range constraint. - Maximum of Minimums: When multiple modules in a dependency graph request different minimum versions of the same dependency, MVS selects the maximum of those requested minimum versions.
- High-Water Mark Determinism: The resulting selected version is guaranteed to satisfy all minimum requirements without requiring complex backtracking or unexpected automatic major upgrades.
2. Step-by-Step Diamond Dependency Graph Resolution
A classic challenge in module dependency management is the "Diamond Dependency" pattern. Consider a scenario where a primary module user depends directly on both common and weather, while weather itself depends transitively on an updated version of common.
Graph Structure
+-------------------+
| proto/user | (Root Module)
+---------+---------+
|
+-------------+-------------+
| |
v v
+---------------------+ +---------------------+
| github.com/acme/ | | github.com/acme/ |
| common v1.2.0 | | weather v2.1.0 |
+---------------------+ +----------+----------+
|
v
+---------------------+
| github.com/acme/ |
| common v1.3.0 |
+---------------------+Step-by-Step MVS Traversal
EasyP begins by parsing the root module's protobuf.mod file (proto/user/protobuf.mod) and recursively visiting all transitive requirements:
- Root
userrequiresgithub.com/acme/common v1.2.0. - Root
userrequiresgithub.com/acme/weather v2.1.0. - Traversal inspects
github.com/acme/weather v2.1.0'sprotobuf.mod, discovering a requirement forgithub.com/acme/common v1.3.0.
Per-Module Isolation Guarantee
Unlike workspace-wide resolvers, EasyP resolves dependencies on a per-module basis. If your repository contains separate service modules under proto/user and proto/order:
proto/usercan resolvecommon v1.3.0.proto/ordercan independently resolvecommon v2.0.0.
Because each proto module maintains its own protobuf.mod and protobuf.lock, teams can update shared dependencies at their own pace without breaking neighboring services in a monorepo.
3. protobuf.mod DSL Directives
The protobuf.mod file resides in the root directory of a Protobuf module. It is a tool-neutral specification focused exclusively on dependency declaration.
// proto/user/protobuf.mod
module github.com/acme/user
roots (
proto
)
require (
github.com/googleapis/googleapis v1.5.0
github.com/acme/weather v1.2.3
github.com/grpc-ecosystem/grpc-gateway v2.19.0 // indirect
)
// Local debug override
replace github.com/acme/weather => ../weather-forkDirective Reference
module: Declares the canonical module identity (URL/path). Optional when the Git URL matches repository layout.roots: Defines the inner directory root containing.protoschema files (defaults to.). This ensures imports resolve cleanly without requiring prefix manipulation.require: Declares direct and indirect module dependencies with explicit Semantic Version tags.// indirect: Appended automatically byeasyp mod tidyfor transitive dependencies not directly imported by the root module's.protofiles.
replace: Overrides a module dependency with a local file path (../weather-fork) or an alternative Git repository/version. Useful for local development and debugging.
Active replace directives are strictly intended for local development. EasyP CI workflows will flag and reject any commit containing an active replace directive.
4. Hermetic protobuf.lock Mechanism
The protobuf.lock file is an auto-generated artifact created and maintained by EasyP commands (easyp mod tidy and easyp mod update).
# protobuf.lock - GENERATED FILE, DO NOT EDIT MANUALLY
version: 1
modules:
- source: github.com/googleapis/googleapis
version: v1.5.0
commit: 8f3a1c94b2e10a84e
hash: h1:a9F8zK...
- source: github.com/acme/weather
version: v1.2.3
commit: 51d2e071c89f33b11
hash: h1:x7B3mL...
- source: github.com/grpc-ecosystem/grpc-gateway
version: v2.19.0
commit: c4f88aa91024e12c4
hash: h1:p0Q1wK...Guaranteed Hermetic Builds
- Strict Locking: CI pipelines execute
easyp mod downloadagainstprotobuf.lock. Neither network fluctuations nor registry state can change the downloaded schema versions. - Digest Verification: The
hashattribute validates content integrity against cache tampering or force-pushed Git tags. - Explicit Lifecycle:
protobuf.lockis only updated via explicit developer execution ofeasyp mod tidyoreasyp mod update.
5. Architectural Comparison
| Feature / Model | EasyP MVS | Go Modules (MVS) | Cargo / Rust (PubGrub SAT) | Buf v2 (Workspace SVS) |
|---|---|---|---|---|
| Algorithm | Minimal Version Selection | Minimal Version Selection | SAT Solver (PubGrub) | Workspace Single Version |
| Selection Rule | max(minimums) | max(minimums) | Complex SemVer Ranges (^1.2.3) | Global Single Version |
| Dependency Scope | Per-module | Per-module | Per-crate | Entire Workspace |
| Multiple Versions in Binary | Forbidden (FQN collision) | Allowed across major versions | Allowed across major versions | Forbidden |
| Lockfile | protobuf.lock | go.sum | Cargo.lock | buf.lock |
| Local Override | replace directive | replace directive | [patch] / path | Local workspace module |
Why MVS Fits Protobuf Schemas Best
Protobuf schemas enforce strict package name and message Fully Qualified Name (FQN) uniqueness within a compilation unit. Unlike Rust (where Cargo can rename crates under the hood) or Go (where v2 uses a distinct import path), compiling two versions of user.v1.User into the same protoc descriptor set causes immediate FQN collision errors.
EasyP's per-module MVS provides the exact balance required for Protobuf: deterministic single-version resolution within each module graph, paired with complete version independence between separate service modules.