11 December 2015
Major version updates to libraries solve the API warts of old and bring shiny new APIs to address previous shortcomings—often in a breaking fashion. Updating an Android or Java app is usually a day or two affair before you reap the benefits. Problems arise, however, when other libraries you depend on have transitive dependencies on older versions of the updated library.
Retrofit 2.0 is nearing release and it comes with three years of knowledge gained since its version 1.0—some of which is in backwards-incompatible API changes. We are fortunate to say that Retrofit has become a popular library, but it presents a real problem in that other libraries have been published which rely on its 1.x API. While a sudden breaking change doesn’t present an immediate problem for them, consumers of those libraries wanting to upgrade their apps to the new API face a difficult choice.
This problem is not new, and I won’t waste time rehashing all its nuances. After some discussion with Jesse Wilson, we have decided on a course of action for the libraries we manage going forward in order to mitigate this pain. The following does not assume strict semantic versioning, but general adherence to its idea of major version bumps.
For major version updates in significantly foundational libraries we will take the following steps:
Rename the Java package to include the version number.
This immediately solves the API compatibility problem from transitive dependencies on multiple versions. Classes from each can be loaded on the same classpath without interacting negatively.
Users can perform major versions updates gradually or in increments rather than requiring an immediate switch. If possible, shims for older versions can be built on newer versions in a sibling artifact.
For example, versions 0.x and 1.x would be under com.example.retrofit
, versions 2.x would be under com.example.retrofit2
, and so on.
(Libraries with a major version of 0 or 1 can skip this, and only start with major version 2 and above.)
Include the library name as part the group ID in the Maven coordinates.
Even for projects that have only a single artifact, including the project name in the group ID allows future updates that may introduce additional artifacts to not pollute the root namespace. In projects that have multiple artifacts from inception, it provides a means of grouping them together on artifact hosts like Maven central.
For example, the Maven coordinates for the main Retrofit artifact could be com.example.retrofit:retrofit
. Additional modules (present or future) can be listed under the same group ID such as com.example.retrofit:converter-moshi
.
Rename the group ID in the Maven coordinates to include the version number.
Individual group IDs prevent dependency resolution semantics to upgrade older versions to newer, incompatible ones. Each major version is resolved independently allowing transitive dependencies to be upgraded compatibly.
For example, take a project given Library A with a dependency on 1.2.0, Library B with a dependency on 1.3.0, Library C with a dependency on 2.1.0, and a direct dependency on 2.4.0. A dependency resolver would first choose 1.3.0 for which Library A and Library B are compatible using the 1.x group ID. The resolver would then choose 2.4.0 for which Library C is compatible using the 2.x group ID.
Group ID renaming is chosen over the artifact ID for a few reasons:
retrofit2-2.1.0
).pom.xml
changes would be required instead of one group ID change.(Libraries with a major version of 0 or 1 can skip this, and only start with major version 2 and above.)
Each of these steps are not new ideas themselves. The growing usage of the libraries on which we work has forced us to figure out a reasonable policy to ensure major version upgrades are as smooth as possible. We are excited to offer something that will allow our users to upgrade sooner while also having relatively low maintenance cost for us.
The forthcoming releases of Retrofit 2.0 and OkHttp 3.0 will be the first two libraries to apply this policy. Enjoy!
— Jake Wharton