The diamond dependency problem occurs when a top-level package depends on multiple packages that share a common dependency, but require different versions of it.
OptimalControl (A)
/ | \
/ | \
CTDirect CTFlows CTParser (B, D, E)
\ | /
\ | /
CTBase (C)
Problem: When CTBase releases a breaking change (v1 → v2), how do we test OptimalControl with updated packages without breaking the ecosystem?
Julia's Pkg.jl resolver follows this order:
- Pre-release Filtering: By default, pre-releases are completely ignored
- Constraint Intersection: Find versions satisfying all dependencies
- Stability Preference: Prefer stable over pre-release when both available
- Maximum Selection: Choose highest version within stability class
Key insight: Pre-releases are invisible unless explicitly allowed in compat.
# This IGNORES all v2 pre-releases
[compat]
CTBase = "1"
# This ALLOWS v2 pre-releases
[compat]
CTBase = "1, 2.0.0-"The resolver finds the intersection of all constraints:
# Example
OptimalControl requires: CTBase ∈ {v1}
CTDirect requires: CTBase ∈ {v2}
# Intersection: {v1} ∩ {v2} = ∅ → UNSATISFIABLE ❌When intersection contains both stable and pre-release:
Intersection = {v1, v2-beta.1} → Choose v1 (stable preferred)
Intersection = {v2-beta.1} → Choose v2-beta.1 (only option)- Initial state: All packages use CTBase v1
- CTBase v2 released (breaking change)
- CTDirect updated to require CTBase v2
- Try to test OptimalControl with new CTDirect
Result:
OptimalControl v1: CTBase ∈ {v1}
CTDirect v2: CTBase ∈ {v2}
# Intersection: ∅ → UNSATISFIABLE ❌Cannot test integration!
Before releasing CTBase v2, widen compat in all dependent packages:
# OptimalControl, CTDirect, CTFlows, CTParser
[compat]
CTBase = "1, 2.0.0-" # Accept v1 AND v2 pre-releasesEffect: Users still get v1 (stable preferred), but v2 betas are now allowed.
# Release CTBase v2-beta.1User installation:
Pkg.add("OptimalControl")
# Intersection: {v1, v2-}
# Available: {v1, v2-beta.1}
# Choose: v1 (stable preferred) ✅No disruption for users!
# CTDirect v2
[compat]
CTBase = "2.0.0-" # Require v2 betaIntegration test:
# OptimalControl v1.1 + CTDirect v2
OptimalControl: CTBase ∈ {v1, v2-} ← Widened
CTDirect: CTBase ∈ {v2-} ← Forces beta
# Intersection: {v2-}
# Available: {v2-beta.1}
# Choose: v2-beta.1 ✅Integration testing now works!
# Release CTBase v2 (stable)Resolution:
# With updated packages
Intersection: {v2-}
Available: {v2-beta.1, v2}
Choose: v2 (stable preferred) ✅Without beta:
Intersection = ∅ → UNSATISFIABLE
With beta:
Intersection = {v2-beta} → SATISFIABLE
The resolver prefers stable versions, so:
- Regular users get v1 (stable)
- Developers testing new versions get v2-beta (only option in intersection)
- After v2 stable release, everyone migrates smoothly
Time 1: Everyone on v1 (stable)
Time 2: Beta available, users still on v1
Time 3: Some packages migrate to v2-beta
Time 4: v2 stable released
Time 5: Ecosystem migrates to v2
# Widen compat in all dependent packages
# CTDirect, CTFlows, CTParser, CTModels, OptimalControl[compat]
CTBase = "1, 2.0.0-"cd CTBase
# Make breaking changes
# Update version to 2.0.0-beta.1
# Registercd CTDirect
# Adapt to CTBase v2[compat]
CTBase = "2.0.0-" # Require v2using Pkg
Pkg.develop(path="path/to/OptimalControl")
Pkg.test("OptimalControl")
# Will use CTBase v2-beta.1 ✅cd CTBase
# Update version to 2.0.0
# Register- Pre-releases are invisible unless explicitly allowed in compat
- Widening creates paths that didn't exist before
- Betas don't replace stables - they're fallback options
- Preventive action is essential - widen before releasing beta
- Users are protected by stable preference in resolver
The beta strategy solves the diamond dependency problem by:
- Widening compat preventively to allow future betas
- Using betas as resolution paths during migration
- Protecting users through resolver's stable preference
- Enabling gradual migration across the ecosystem
This allows independent package development while maintaining ecosystem stability.