Skip to content(if available)orjump to list(if available)

We shouldn't have needed lockfiles

We shouldn't have needed lockfiles

91 comments

·August 6, 2025

hyperpape

> But if you want an existence proof: Maven. The Java library ecosystem has been going strong for 20 years, and during that time not once have we needed a lockfile. And we are pulling hundreds of libraries just to log two lines of text, so it is actively used at scale.

Maven, by default, does not check your transitive dependencies for version conflicts. To do that, you need a frustrating plugin that produces much worse error messages than NPM does: https://ourcraft.wordpress.com/2016/08/22/how-to-read-maven-....

How does Maven resolve dependencies when two libraries pull in different versions? It does something insane. https://maven.apache.org/guides/introduction/introduction-to....

Do not pretend, for even half a second, that dependency resolution is not hell in maven (though I do like that packages are namespaced by creators, npm shoulda stolen that).

potetm

The point isn't, "There are zero problems with maven. It solves all problems perfectly."

The point is, "You don't need lockfiles."

And that much is true.

(Miss you on twitter btw. Come back!)

hyperpape

I think Maven's approach is functionally lock-files with worse ergonomics. You can only use the dependency from the libraries you use, but you're waiting for those libraries to update.

As an escape hatch, you end up doing a lot of exclusions and overrides, basically creating a lockfile smeared over your pom.

P.S. Sadly, I think enough people have left Twitter that it's never going to be what it was again.

potetm

Of course it's functionally lock files. They do the same thing!

There's a very strong argument that manually managing deps > auto updating, regardless of the ergonomics.

P.S. You're, right, but also it's where the greatest remnant remains. :(

jeltz

You don't need package management by the same token. C is proof of that.

Having worked professionally in C, Java, Rust, Ruby, Perl, PHP I strongly prefer lock files. They make it so much nicer to manage dependencies.

potetm

"There is another tool that does exactly the job of a lockfile, but better."

vs

"You can use make to ape the job of dependency managers"

wat?

trjordan

There is absolutely a good reason for version ranges: security updates.

When I, the owner of an application, choose a library (libuseful 2.1.1), I think it's fine that the library author uses other libraries (libinsecure 0.2.0).

But in 3 months, libinsecure is discovered (surprise!) to be insecure. So they release libinsecure 0.2.1, because they're good at semver. The libuseful library authors, meanwhile, are on vacation because it's August.

I would like to update. Turns out libinsecure's vulnerability is kind of a big deal. And with fully hardcoded dependencies, I cannot, without some horrible annoying work like forking/building/repackaging libuseful. I'd much rather libuseful depend on libinsecure 0.2.*, even if libinsecure isn't terribly good at semver.

I would love software to be deterministically built. But as long as we have security bugs, the current state is a reasonable compromise.

deredede

What if libinsecure 0.2.1 is the version that introduces the vulnerability, do you still want your application to pick up the update?

I think the better model is that your package manager let you do exactly what you want -- override libuseful's dependency on libinsecure when building your app.

tonsky

It’s totally fine in Maven, no need to rebuild or repackage anything. You just override version of libinsecure in your pom.xml and it uses the version you told it to

zahlman

So you... manually re-lock the parts you need to?

deredede

Sure, I'm happy with locking the parts I need to lock. Why would I lock the parts I don't need to lock?

aidenn0

Don't forget the part where Maven silently picks one version for you when there are transitive dependency conflicts (and no, it's not always the newest one).

epage

Let's play this out in a compiled language like Cargo.

If every dependency was a `=` and cargo allowed multiple versions of SemVer compatible packages.

The first impact will be that your build will fail. Say you are using `regex` and you are interacting with two libraries that take a `regex::Regex`. All of the versions need to align to pass `Regex` between yourself and your dependencies.

The second impact will be that your builds will be slow. People are already annoyed when there are multiple SemVer incompatible versions of their dependencies in their dependency tree, now it can happen to any of your dependencies and you are working across your dependency tree to get everything aligned.

The third impact is if you, as the application developer, need a security fix in a transitive dependency. You now need to work through the entire bubble up process before it becomes available to you.

Ultimately, lockfiles are about giving the top-level application control over their dependency tree balanced with build times and cross-package interoperability. Similarly, SemVer is a tool any library with transitive dependencies [0]

[0] https://matklad.github.io/2024/11/23/semver-is-not-about-you...

matklad

This scheme _can_ be made to work in the context of Cargo. You can have all of:

* Absence of lockfiles

* Absence of the central registry

* Cryptographically checksummed dependency trees

* Semver-style unification of compatible dependencies

* Ability for the root package to override transitive dependencies

At the cost of

* minver-ish resolution semantics

* deeper critical path in terms of HTTP requests for resolving dependencies

The trick is that, rather than using crates.io as the universe of package versions to resolve against, you look only at the subset of package versions reachable from the root package. See https://matklad.github.io/2024/12/24/minimal-version-selecti...

junon

You should not be editing your cargo.lock file manually. Cargo gives you a first-class way of overriding transitive dependencies.

oblio

Java is compiled, FYI.

hosh

Wasn’t the article suggesting that the top level dependencies override transitive dependencies, and that could be done in the main package file instead of the lock file?

andy99

In case the author is reading, I can't read your article because of that animation at the bottom. I get it, it's cute, but it makes it too distracting to concentrate on the article, so I ended up just closing it.

fennecbutt

It also covers a whole 1/4 of the screen on mobile...

somehnguy

I read the article but that animation was incredibly distracting. I don't even understand what it's for - clicking it does nothing. Best guess is a representation of how many people active on page.

lalaithion

What if your program depends on library a1.0 and library b1.0, and library a1.0 depends on c2.1 and library b1.0 depends on c2.3? Which one do you install in your executable? Choosing one randomly might break the other library. Installing both _might_ work, unless you need to pass a struct defined in library c from a1.0 to b1.0, in which case a1.0 and b1.0 may expect different memory layouts (even if the public interface for the struct is the exact same between versions).

The reason we have dependency ranges and lockfiles is so that library a1.0 can declare "I need >2.1" and b1.0 can declare "I need >2.3" and when you depend on a1.0 and b1.0, we can do dependency resolution and lock in c2.3 as the dependency for the binary.

tonsky

One of the versions will be picked up. If that version doesn’t work, you can try another one. The process is exactly the same

deredede

Alternative answer: both versions will be picked up.

It's not always the correct solution, but sometimes it is. If I have a dependency that uses libUtil 2.0 and another that uses libUtil 3.0 but neither exposes types from libUtil externally, or I don't use functions that expose libUtil types, I shouldn't have to care about the conflict.

Joker_vD

> If that version doesn’t work, you can try another one.

And how will this look like, if your app doesn't have library C mentioned in its dependencies, only libraries A and B? You are prohibited from answering "well, just specify all the transitive dependencies manually" because it's precisely what a lockfile is/does.

tonsky

Maven's version resolution mechanism determines which version of a dependency to use when multiple versions are specified in a project's dependency tree. Here's how it works:

- Nearest Definition Wins: When multiple versions of the same dependency appear in the dependency tree, the version closest to your project in the tree will be used.

- First Declaration Wins: If two versions of the same dependency are at the same depth in the tree, the first one declared in the POM will be used.

deredede

It's not "all the transitive dependencies". It's only the transitive dependencies you need to explicitly specify a version for because the one that was specified by your direct dependency is not appropriate for X reason.

boscillator

Ok, but what happens when lib-a depends on lib-x:0.1.4 and lib-b depends on lib-x:0.1.5, even though it could have worked with any lib-x:0.1.*? Are these libraries just incompatible now? Lockfiles don't guarantee that new versions are compatible, but it guarantees that if your code works in development, it will work in production (at least in terms of dependencies).

I assume java gets around this by bundling libraries into the deployed .jar file. That this is better than a lock file, but doesn't make sense for scripting languages that don't have a build stage. (You won't have trouble convincing me that every language should have a proper build stage, but you might have trouble convincing the millions of lines of code already written in languages that don't.)

aidenn0

> I assume java gets around this by bundling libraries into the deployed .jar file. That this is better than a lock file, but doesn't make sense for scripting languages that don't have a build stage. (You won't have trouble convincing me that every language should have a proper build stage, but you might have trouble convincing the millions of lines of code already written in languages that don't.)

You are wrong; Maven just picks one of lib-x:0.1.4 or lib-x:0.1.5 depending on the ordering of the dependency tree.

simonw

I see lockfiles as something you use for applications you are deploying - if you run something like a web app it's very useful to know exactly what is being deployed to production, make sure it exactly matches staging and development environments, make sure you can audit new upgrades to your dependencies etc.

This article appears to be talking about lockfiles for libraries - and I agree, for libraries you shouldn't be locking exact versions because it will inevitably pay havoc with other dependencies.

Or maybe I'm missing something about the JavaScript ecosystem here? I mainly understand Python.

aidenn0

I think you missed the point of the article. Consider Application A, that depends on Library L1. Library L1 in turn depends on Library L2:

A -> L1 -> L2

They are saying that A should not need a lockfile because it should specify a single version of L1 in its dependencies (i.e. using an == version check in Python), which in turn should specify a single version of L2 (again with an == version check).

Obviously if everybody did this, then we wouldn't need lockfiles (which is what TFA says). The main downsides (which many comments here point out) are:

1. Transitive dependency conflicts would abound

2. Security updates are no longer in the hands of the app developers (in my above example, the developer of A1 is dependent on the developer of L1 whenever a security bug happens in L2).

3. When you update a direct dependency, your transitive dependencies may all change, making what you that was a small change into a big change.

(FWIW, I put these in order of importance to me; I find #3 to be a nothingburger, since I've hardly ever updated a direct dependency without it increasing the minimum dependency of at least one of its dependencies).

hosh

Is the article also suggesting that if there are version conflicts, it goes with the top level library? For example, if we want to use a secure version of L2, it would be specified at A, ignoring the version specified by L1?

Or maybe I misread the article and it did not say that.

aidenn0

It's maybe implied since Maven lets you do that (actually it uses the shallowest dependency, with the one listed first winning ties), but the thrust of the article seems to be roughly: "OMGWTFBBQ we can't use L2 with 0.7.9 if L1 was only tested with 0.7.9!" so I don't know how the author feels about that.

[edit]

The author confirmed that they are assuming Maven's rules and added it to the bottom of their post.

null

[deleted]

kaelwd

The lockfile only applies when you run `npm install` in the project directory, other projects using your package will have their own lockfile and resolve your dependencies using only your package.json.

wedn3sday

I absolutely abhor the design of this site. I cannot engage with the content as Im filled with a deep burning hatred of the delivery. Anyone making a personal site: do not do this.

spooky_deep

> The important point of this algorithm is that it’s fully deterministic.

The algorithm can be deterministic, but fetching the dependencies of a package is not.

It is usually an HTTP call to some endpoint that might flake out or change its mind.

Lock files were invented to make it either deterministic or fail.

Even with Maven, deterministic builds (such as with Bazel) lock the hashes down.

This article is mistaken.

ratelimitsteve

anyone find a way to get rid of the constantly shifting icons at the bottom of the screen? I'm trying to read and the motion keeps pulling my attention away from the words toward the dancing critters.

foobarbecue

$("#presence").remove()

And yeah, I did that right away. Fun for a moment but extremely distracting.

zahlman

I use NoScript, which catches all of these sorts of things by default. I only enable first-party JS when there's a clear good reason why the site should need it, and third-party JS basically never beyond NoScript's default whitelist.

karmakurtisaani

Agreed. It's an absolutely useless feature for me to see as well.

null

[deleted]

vvillena

Reader mode.

trinix912

Block ###presence with UBlock.

andix

Lockfiles are essential for somewhat reproducible builds.

If a transient dependency (not directly referenced) updates, this might introduce different behavior. if you test a piece of software and fix some bugs, the next build shouldn't contain completely different versions of dependencies. This might introduce new bugs.

tonsky

> Lockfiles are essential for somewhat reproducible builds.

No they are not. Fully reproducible builds have existed without lockfiles for decades

pluto_modadic

...source?

show me one "decades old build" of a major project that isn't based on 1) git hashes 2) fixed semver URLs or 3) exact semver in general.

its-summertime

of distros, they usually refer to an upstream by hash

https://src.fedoraproject.org/rpms/conky/blob/rawhide/f/sour...

also of flathub

https://github.com/flathub/com.belmoussaoui.ashpd.demo/blob/...

"they are not lockfiles!" is a debatable separate topic, but for a wider disconnected ecosystem of sources, you can't really rely on versions being useful for reproducibility

andix

> they usually refer to an upstream by hash

exactly the same thing as a lockfile

andix

Sure, without package managers.

It's also not about fully reproducible builds, it's about a tradeoff to get modern package manger (npm, cargo, ...) experience and also somewhat reproducible builds.

jedberg

The entire article is about why this isn't the case.

andix

It suggests a way more ridiculous fix. As mentioned by other comments in detail (security patches for transient dependencies, multiple references to the same transient dependency).

hosh

Many comments talk about how top-level and transitive dependencies can conflict. I think the article is suggesting you can resolve those by specifying them in the top-level packages and overriding any transitive package versions. If we are doing that anyways, it circles back to if lock files are necessary.

Given that, I still see some consequences:

The burden for testing if a library can use its dependency falls back on the application developer instead of the library developer. A case could be made that, while library developers should test what their libraries are compatible with, the application developer has the ultimate responsibility for making sure everything can work together.

I also see that there would need to be tooling to automate resolutions. If ranges are retained, the resolver needs to report every conflict and force the developer to explicitly specify the version they want at the top-level. Many package managers automatically pick one and write it into the lock file.

If we don’t have lock files, and we want it to be automatic, then we can have it write to the top level package manager and not the lock file. That creates its own problems.

One of those problems comes from humans and tooling writing to the same configuration file. I have seen problems with that idea pop up — most recently, letting letsencrypt modify nginx configs, and now I have to manually edit those. Letsencrypt can no longer manage them. Arguably, we can also say LLMs can work with that, but I am a pessimist when it comes to LLM capabilities.

So in conclusion, I think the article writer’s reasoning is sound, but incomplete. Humans don’t need lockfiles, but our tooling need lockfiles until it is capable of working with the chaos of human-managed package files.

hosh

If the dependencies are specified as data, such as package.json, or a yaml or xml file, it may be structured enough that tools can still manage it. Npm install has a save flag that lets you do that. Python dep files may be structured enough to do this as well.

If the package specification file is code and not data, then this becomes more problematic. Elixir specified dep as data within code. Arguably, we can add code to read and write from a separate file… but at that point, those might as well be lock files.