When to pin your project's dependencies?

When to pin your project's dependencies?

Dependency management can be a source of frustration for developers, especially when working in large teams with multiple packages depending on each other. This is commonly known as dependency hell. "Pinning" (or "Freezing") dependencies can help mitigate these frustrations when used correctly.

What is dependency pinning?

Dependency pinning is restricting a package's version in your software to a certain range of versions. These ranges are specified in a package manager's configuration file, like package.json or requirements.txt. These ranges can be very lax, to the point of installing any version of the dependency, to very specific, down to install an exact version of the dependency.

For small projects this is not a big deal. However, it can get messy when an organization creates multiple shared libraries that are consumed by many different applications. Dependencies that are too specific can add friction when trying to update packages and can cause critical updates to be missed. Dependencies that are too lax can cause long resolution time of packages and unpredictable environments from machine to machine.

It can be hard to know when to use a specific version, or a more lax version range, but here are some rules to follow that will help your team scale with many projects and libraries.

To pin or not to pin?

This is a common question for still learning developers. Pinning a dependency has many advantages. It keeps your environments consistent, and helps keep your development environment as close to your production environment. When you install dependencies in your development environment, the package versions will be the same version as what is in production. Pinning also keeps your environments predictable, in the sense that you can be more confident in what packages your code is running.

This is good until you try to update a dependency. When your package manager resolves all of the dependencies in your project, it must ultimately choose one package. If you have two dependencies that in turn require the same package, but different versions, you will run into conflicts when trying to resolve dependencies. This problem is known as the diamond dependency problem

Keeping your dependencies open and non-restrictive can avoid this issue, since both of the dependencies are flexible to the package version they need, and the package manager can choose one that satisfies both dependencies. However, by opening up your dependencies, you lose some of the benefits of pinning, namely consistency and predictability among environments. It can be very frustrating when your automated build fails not because of your code changes, but because the package manager decided to use a different package version that introduced a bug.

So, which method is better to follow? In general, this is the rule I follow:

Keep your applications as strict as possible, and keep your libraries as lax as possible.

What is the difference between an application and a library?. In general, an application is the closest code to your end-user, and is not a dependency to anything else. A library on the other hand, is code that is a dependency on other code, though it may contain code that is shared among other applications or libraries.

By pinning your application's dependencies as much as possible, you are keeping the code that actually runs on your servers/clients as predictable as possible. This is important when you need to update a package because of security vulnerabilities, and you want to make absolutely certain that the version you specify is the one that is run.

By keeping your libraries' dependencies as lax as possible (usually pinning at the Major version level, to avoid breaking changes at future major version), you ensure that the library is compatible with as many other projects as possible.

What about lock files?

Some package managers come with the ability to "freeze" or "lock" dependencies at specific versions. How they work is that you generally provide a dependency file that is fairly lax in dependency pinning. When you use the package manager to resolve and install the dependencies, it will then create a lock file that contains the resolved dependencies. This file can then be used on other developers machines or a build pipeline to perfectly recreate the environment. This allows you to maintain a smaller set of critical dependencies that is easier to manage, while still having the strictness of a restrictive dependency pinning.

Conclusion

As with any rule, there will be exceptions. However, by generally following this rule, your team will be able to avoid headaches and frustrations when maintaining your software's dependencies.

Show Comments