A dependency is never free. Every package added to a codebase becomes part of the software that team now owns — not in a legal sense, but in the sense that matters at 2 a.m. when something breaks: nobody's users care that the failure originated three layers down in someone else's code.
Need a form? Add a package. Need validation? Add a package. Need a small utility? Add a package. Every modern package ecosystem is engineered to make this frictionless — a search, an install command, and the feature works. That's a genuine strength of modern tooling. It's also exactly why the cost of a dependency doesn't show up when it's added. It shows up later, and by then it's spread across a dependency tree nobody fully mapped at the time.
Adding a package doesn't just add functionality — it adds another maintainer whose priorities aren't yours, another release cycle to track, another surface for vulnerabilities to enter through, another set of licensing terms to be aware of, and an entire transitive dependency tree that's usually far larger than the one direct import suggests. The question worth asking isn't just "can this package solve my problem." It's "is this package worth owning" — because functionally, once it's in the tree, it is owned, whether or not anyone signed up for that explicitly.
The pattern is familiar: need a feature, search the registry, install the package, problem solved — and for a while, everything genuinely does feel more productive. Six months later, that single dependency has pulled in three or four more, one of them is carrying a known vulnerability, and the team is spending its time resolving version conflicts instead of shipping anything new. The convenience didn't disappear. It moved into the future, compounding the whole time nobody was looking at it.
Most developers who add a single package believe they've added one thing. They haven't — they've added everything that package depends on, and everything those dependencies depend on in turn. A direct dependency can be actively maintained and well-audited while something four layers beneath it is abandoned, unmaintained, or quietly carrying a disclosed CVE nobody upstream has patched yet. This is precisely why dependency management gets harder as a system grows: the risk isn't concentrated in the choices a team made directly. It's distributed across choices made by people that team has never heard of, several layers removed from any decision they actually made.
It's tempting to think of dependency sprawl as a JavaScript ecosystem quirk, or something specific to whichever framework happens to be top of mind — but the pattern repeats identically across every ecosystem that makes installation easy, which today is essentially all of them. A Flutter developer reaching for a package to avoid writing a form manually inherits state-management assumptions and upgrade constraints along with it. A .NET team adding NuGet packages without discipline ends up fighting version conflicts and security-scanning noise that has nothing to do with the actual feature those packages were meant to deliver. Python's pip, Rust's Cargo, Go's module system, Ruby's gems — every one of them optimizes for the same frictionless install experience, and every one of them produces the same long-term liability when that ease isn't paired with actual judgment about what's being pulled in.
Two real incidents make the abstract risk concrete. In 2016, a developer unpublished a package called left-pad from npm — eleven lines of code, no dependencies of its own — after a dispute over an unrelated package name. Thousands of projects, including foundational tools like Babel and React, failed to build within minutes, because a trivial utility had quietly become part of an enormous number of dependency trees without anyone auditing how deep the reliance actually went. In 2024, a contributor who had spent nearly two years patiently building trust within the xz-utils project was handed maintainer access and used it to insert a deliberate backdoor into a compression library present on nearly every major Linux distribution — caught only because a Microsoft engineer happened to notice SSH logins running half a second slower than expected during routine benchmarking. Neither incident required a team to write bad code. Both required only that a team depend on something they didn't fully own the risk profile of.
A useful test: could this be implemented clearly, in a few lines of code the team can actually read and maintain? A small validation helper, a simple mapping function, a basic date utility, a thin wrapper around one API call — these are frequently faster to write directly than to evaluate, install, and then maintain as an external dependency for years. Reaching for a package to solve a problem that takes minutes to solve directly is a trade: minutes saved today against years of upgrade cycles, security advisories, and version conflicts accepted on behalf of a future team that never got a say in the decision.
It's often true that a problem has already been solved by someone else. That's not, on its own, a reason to adopt their solution. A mature decision weighs the complexity actually being added, who on the team will own understanding it long-term, how familiar the team already is with it, what it does to the security surface, and how often it needs attention — not just whether a working package exists somewhere in a registry.
Good reasons to add a dependency: complex cryptography that shouldn't be reimplemented by hand, standards implementations, database drivers, mature frameworks, genuinely difficult algorithms, anything requiring specialist expertise a team doesn't have and doesn't need to build in-house. Weak reasons: replacing ten lines of straightforward code, avoiding the work of actually understanding a problem, following whatever's popular this year, or copying another project's dependency list without asking whether the reasoning still applies.
When an application depends on a package and that package fails, users experience exactly one thing: the system they were using stopped working. They don't see, and don't care, that the root cause traces back to code the team never wrote. Depending on external code doesn't transfer responsibility for what happens when it breaks — it just changes who has the least visibility into why.
Anyone can run an install command. The actual skill is knowing when not to, when to remove a dependency that's stopped earning its place, when to build something small internally instead, and when accepting external ownership is genuinely the right trade. The best engineers aren't the ones who know the most libraries by name. They're the ones who understand what each dependency actually costs before it's added, not after it's already load-bearing.
A codebase full of dependencies isn't automatically sophisticated — sometimes it's just a large, distributed collection of future maintenance work, waiting for the day one of those hundred packages stops being maintained, gets compromised, or breaks in a way nobody on the team is positioned to fix quickly. Use libraries where they provide real, specific value. Skip them where a clear, small, owned solution is genuinely enough. Every package is a decision made on behalf of whoever maintains this system next — including, often, the same team that added it, several years and several context-switches later.
Too Many Security Options Can Become a Security Problem
A standard offering unlimited choices without strong defaults can build weaker systems than one offering fewer, better-recommended ones. Why FAPI narrowed OAuth's options instead of adding more, and what the paradox of choice has to do with it.
Security Through Obscurity: The Oldest Bad Idea That Refuses to Die
From Kerckhoffs' 1883 principle to obscured APIs and JWTs done wrong — where security-through-obscurity comes from, why it fails, and the narrow cases where it still earns a place.