The simplest version declaration is a simple string representing the version to use. Gradle supports different ways of declaring a version string:

  • An exact version: e.g. 1.3, 1.3.0-beta3, 1.0-20150201.131010-1

  • A Maven-style version range: e.g. [1.0,), [1.1, 2.0), (1.2, 1.5]

    • The [ and ] symbols indicate an inclusive bound; ( and ) indicate an exclusive bound.

    • When the upper or lower bound is missing, the range has no upper or lower bound.

    • The symbol ] can be used instead of ( for an exclusive lower bound, and [ instead of ) for exclusive upper bound. e.g ]1.0, 2.0[

    • An upper bound exclude acts as a prefix exclude. This means that [1.0, 2.0[ will also exclude all versions starting with 2.0 that are smaller than 2.0. For example versions like 2.0-dev1 or 2.0-SNAPSHOT are no longer included in the range.

  • A prefix version range: e.g. 1.+, 1.3.+

    • Only versions exactly matching the portion before the + are included.

    • The range + on it’s own will include any version.

  • A latest-status version: e.g. latest.integration, latest.release

  • A Maven SNAPSHOT version identifier: e.g. 1.0-SNAPSHOT, 1.4.9-beta1-SNAPSHOT

Version ordering

Versions have an implicit ordering. Version ordering is used to:

  • Determine if a particular version is included in a range.

  • Determine which version is 'newest' when performing conflict resolution (watch out though, conflict resolution uses "base versions").

Versions are ordered based on the following rules:

  • Each version is split into it’s constituent "parts":

    • The characters [. - _ +] are used to separate the different "parts" of a version.

    • Any part that contains both digits and letters is split into separate parts for each: 1a1 == 1.a.1

    • Only the parts of a version are compared. The actual separator characters are not significant: 1.a.1 == 1-a+1 == 1.a-1 == 1a1 (watch out though, in the context of conflict resolution there are exceptions to this rule).

  • The equivalent parts of 2 versions are compared using the following rules:

    • If both parts are numeric, the highest numeric value is higher: 1.1 < 1.2

    • If one part is numeric, it is considered higher than the non-numeric part: 1.a < 1.1

    • If both are non-numeric, the parts are compared alphabetically, in a case-sensitive manner: 1.A < 1.B < 1.a < 1.b

    • A version with an extra numeric part is considered higher than a version without (even when it’s zero): 1.1 < 1.1.0

    • A version with an extra non-numeric part is considered lower than a version without: 1.1.a < 1.1

  • Certain non-numeric parts have special meaning for the purposes of ordering:

    • dev is consider lower than any other non-numeric part: 1.0-dev < 1.0-ALPHA < 1.0-alpha < 1.0-rc.

    • The strings rc, snapshot, final, ga, release and sp are considered higher than any other string part (sorted in this order): 1.0-zeta < 1.0-rc < 1.0-snapshot < 1.0-final < 1.0-ga < 1.0-release < 1.0-sp < 1.0.

    • These special values are NOT case sensitive, as opposed to regular string parts and they do not depend on the separator used around them: 1.0-RC-1 == 1.0.rc.1

Simple version declaration semantics

When you declare a version using the short-hand notation, for example:

build.gradle.kts
dependencies {
    implementation("org.slf4j:slf4j-api:1.7.15")
}
build.gradle
dependencies {
    implementation('org.slf4j:slf4j-api:1.7.15')
}

Then the version is considered a required version which means that it should minimally be 1.7.15 but can be upgraded by the engine (optimistic upgrade).

There is, however, a shorthand notation for strict versions, using the !! notation:

build.gradle.kts
dependencies {
    // short-hand notation with !!
    implementation("org.slf4j:slf4j-api:1.7.15!!")
    // is equivalent to
    implementation("org.slf4j:slf4j-api") {
        version {
           strictly("1.7.15")
        }
    }

    // or...
    implementation("org.slf4j:slf4j-api:[1.7, 1.8[!!1.7.25")
    // is equivalent to
    implementation("org.slf4j:slf4j-api") {
        version {
           strictly("[1.7, 1.8[")
           prefer("1.7.25")
        }
    }
}
build.gradle
dependencies {
    // short-hand notation with !!
    implementation('org.slf4j:slf4j-api:1.7.15!!')
    // is equivalent to
    implementation("org.slf4j:slf4j-api") {
        version {
           strictly '1.7.15'
        }
    }

    // or...
    implementation('org.slf4j:slf4j-api:[1.7, 1.8[!!1.7.25')
    // is equivalent to
    implementation('org.slf4j:slf4j-api') {
        version {
           strictly '[1.7, 1.8['
           prefer '1.7.25'
        }
    }
}

A strict version cannot be upgraded and overrides whatever transitive dependencies originating from this dependency provide. It is recommended to use ranges for strict versions.

The notation [1.7, 1.8[!!1.7.25 above is equivalent to:

  • strictly [1.7, 1.8[

  • prefer 1.7.25

which means that the engine must select a version between 1.7 (included) and 1.8 (excluded), and that if no other component in the graph needs a different version, it should prefer 1.7.25.

Declaring a dependency without version

A recommended practice for larger projects is to declare dependencies without versions and use dependency constraints for version declaration. The advantage is that dependency constraints allow you to manage versions of all dependencies, including transitive ones, in one place.

build.gradle.kts
dependencies {
    implementation("org.springframework:spring-web")
}

dependencies {
    constraints {
        implementation("org.springframework:spring-web:5.0.2.RELEASE")
    }
}
build.gradle
dependencies {
    implementation 'org.springframework:spring-web'
}

dependencies {
    constraints {
        implementation 'org.springframework:spring-web:5.0.2.RELEASE'
    }
}