The Dependency Pact: When It Becomes a Deal with the Devil
Every time I write cargo add something, I like to think I’m simply adding a feature to my project. It’s convenient, it’s fast, it’s modern. But if I’m honest with myself, what I’m really doing is signing a trilateral contract: between me, a library written by a stranger on the internet, and the end user who blindly trusts my software.
And like all contracts we sign without reading, the fine print eventually comes knocking at the door.
The Day Everything Goes to Shit
Picture this scene, which you’ve probably already lived through: it’s 3 PM on a Friday. The client calls you, furious, because the application you deployed last week is losing data. Not just a little data. Important data. Data worth real money.
You open the project, look at the code you wrote. It’s perfect. It compiles, the tests pass, the logic is crystal clear. But there’s a problem: the bug isn’t in YOUR code. It’s in one of the forty dependencies you have in Cargo.toml. Or worse yet, in one of the hundred and fifty transitive dependencies you didn’t even know you had.
Welcome to the true cost of dependencies.
The Two Paths of Inevitability (or: Why We’re All Doomed)
When we add a dependency, we fundamentally do it for two reasons. And neither one is particularly comforting if you think about it carefully.
First reason: I don’t know how to do that thing.
Okay, I admit it. I don’t know how to write a secure HTTP parser. I don’t know how to implement a cryptographic system that isn’t vulnerable to timing attacks. I don’t know how to handle file compression without losing pieces along the way. And so I use reqwest, I use ring, I use flate2.
Is it a wise choice? Of course it is. Only an idiot would try to reinvent cryptography. But here’s the point: if I don’t know how to do that thing, how can I debug it when that thing breaks? How can I understand if the problem is in my code or in the dependency? How can I evaluate whether that panic I’m seeing is their bug or my incorrect usage?
The answer is: I can, but only up to a point, and the mental cost rises exponentially. Sure, I can read stack traces, test different inputs, open issues on GitHub hoping someone responds. But if the problem is truly inside that dependency, I still have to dive into the source code and try to understand what the hell is going on. And guess when I might have to do all this? That’s right: at 3 PM on a Friday, with the client screaming and a deadline approaching.
Second reason: I could do it, but I don’t feel like it.
“Why should I write a UUID generator when there’s already a crate for that?” “Why should I implement a JSON serializer when serde does everything so well?” “Why should I waste time with a template engine when I can use tera?”
Short answer: because maybe that time “wasted” now would have saved you a week of hell later.
The paradox is extraordinary: you avoid writing something because “it would take too much time,” but when that dependency breaks, you’re forced to understand it anyway. Except now you have to do it:
- Under pressure
- With the client bombarding you with emails
- Trying to navigate code written by others, with conventions different from yours
- With the real possibility that you’ll have to rewrite everything from scratch anyway because the dependency is abandonware or the maintainer doesn’t respond
Congratulations: you saved two days three months ago to lose seven now.
The Real Problem Isn’t the Dependency, It’s You
I know, it sounds harsh. But let me explain.
The point isn’t that using dependencies is bad. The point is that we treat them as magic solutions instead of what they really are: code written by fallible human beings that could blow up in your face at any moment.
How many times have you added a dependency without even opening the repository to see what the code looks like? How many times have you only looked at the download count on crates.io thinking “well, if so many people use it, it must be reliable”? How many times have you thought “if there’s a problem I’ll just file an issue on GitHub and someone will fix it”?
Well, maybe the real incompetence isn’t not knowing how to write an HTTP library. Maybe the real incompetence is delegating critical pieces of our application to strangers on the internet without even knowing how they work.
When you add a dependency, you’re not just adding functionality. You’re adding:
- A failure point you don’t control
- An attack surface for bugs you didn’t write
- A comprehension debt you’ll have to pay sooner or later
- A chain of transitive dependencies that could be dozens or hundreds
- A responsibility to your end user that you’ve delegated to a stranger
And the crazy thing? We often don’t even know it. You add a dependency thinking it’s “just that one,” but in reality you’re bringing along an entire ecosystem. That cute 500-line crate you added? It has fifteen dependencies. Those fifteen have another thirty. And suddenly you find yourself with hundreds of thousands of lines of code you’re responsible for but have never read.
Supply Chain Attacks: When Danger Comes from Within
Let’s be clear: when you add a dependency, you’re literally running code written by a stranger on the internet. On your machine. With your permissions. During compilation, during runtime, always.
And I’m not talking about paranoid movie scenarios. In recent years we’ve seen everything: microscopic dependencies removed that crashed entire ecosystems. Maintainers who, tired of working for free for billion-dollar multinationals, deliberately introduced bugs into their projects as a form of protest. Attackers who took control of legitimate projects and injected malware to steal credentials or cryptocurrencies.
“But that happens in other languages,” you’ll say. “We in Rust are different, we have the borrow checker, we have safety!”
Sure. The borrow checker protects you from data races. But tell me: does it protect you from a malicious build.rs that, during compilation, exfiltrates your SSH keys? Does it protect you from a dependency that decides to send telemetry of your data to an unknown server? Does it protect you from a maintainer who, exhausted from unpaid work, sells control of the project to the highest bidder without telling anyone?
The answer is no. The compiler can’t help you if the problem isn’t unsafe code, but the intentions of whoever wrote it.
The software supply chain is the Wild West of cybersecurity. Every dependency is an open door. And the beauty (or the ugliness, depending on your point of view) is that we often don’t even think about it. We add a crate because “it has many stars on GitHub” or “many downloads on crates.io,” as if popularity were a guarantee of security or integrity.
But popularity tells you nothing about who maintains that project, how motivated they are to continue, how resistant they are to corruption or exhaustion. Every dependency you add is a potential Trojan horse. And the scariest thing? You probably wouldn’t even notice until it’s too late.
Abandonment: The Silent Tragedy
Then there’s the more subtle and more common scenario: abandonment.
You find a perfect crate. It does exactly what you need. The code is clean, the tests pass, the documentation is good. You add it to the project. Everything works. For six months, a year, two years.
Then one day you open an issue because you found an edge case. No response. You open another. Silence. You look at the date of the last commit: three years ago. You look at the other open issues: dozens, all without response.
The maintainer has disappeared. Maybe they changed jobs, maybe they lost interest, maybe they simply burned out from having to maintain for free a project used by thousands of companies that never contribute.
And now? The Rust community has started discussing policies for abandoned crates, how to transfer maintenance more smoothly, how to handle these cases. But in the field, when it actually happens to you, the practical result is simple: either you fork the project and maintain it yourself (with all the work that entails), or you replace it (and good luck finding something compatible), or you rewrite it from scratch (what you could have done from the beginning, if only you hadn’t been “lazy”).
In any case, it’s YOU who ends up dealing with the problem.
The Faustian Bargain
Mephistopheles offers Faust universal knowledge in exchange for his soul. Dependencies offer us immediate power in exchange for our future control.
It’s a deal we make constantly, often unconsciously. Cargo add and off we go, we’ve just traded two hours of development today for potential days or weeks of debugging tomorrow.
I’m not saying we should stop using dependencies. That would be stupid and counterproductive. But I am saying we should stop treating them as free and consequence-free solutions.
Every dependency has a cost. The fact that this cost doesn’t manifest immediately doesn’t mean it doesn’t exist. It’s like debt with interest: the more you ignore it, the more it grows.
So? What Do We Do?
I don’t have a magic solution. If I did, I’d probably package it in a crate and put it on crates.io, creating an infinitely ironic meta-dependency.
But I have some thoughts:
First: stop adding dependencies automatically. Every time you’re about to write cargo add, stop and ask yourself: “Do I really need this? Could I write it myself in a reasonable time? If it breaks, am I at least able to navigate the code to understand what’s happening?”.
Careful: I’m not saying you need to be able to reimplement everything. There are critical dependencies - crypto, TLS, database drivers, HTTP stacks - that no one should ever try to write from scratch, but which are maintained by solid teams, with constant auditing and large communities. The point isn’t “if you can’t fix it don’t use it,” but “if no one on the team is even able to navigate that code in an emergency, maybe you’re using it too trustingly.”
Second: if you add a dependency, open it and read it. Not all of it, you’re not crazy. But at least the main files, the public APIs, the fundamental architectural decisions. Spend an afternoon understanding how it works. That afternoon could save you a week of hell in the future.
Third: in your team, every dependency should have a “mental owner.” Someone who has read it, who understands it, who could fork and patch if necessary. If no one wants to take on this responsibility, maybe that dependency is a problem.
Fourth: prefer small, focused, readable dependencies to monolithic monsters. In Rust we’re lucky: the ecosystem tends toward small and composable crates. Exploit this philosophy. A 500-line dependency is infinitely more manageable than one with 50,000.
Fifth: always remember that in the Rust world, where the compiler protects us from so many errors, dependencies remain the last realm of chaos. The borrow checker can’t help you if the problem is in code you didn’t write and don’t understand.
The Real Question
In the end, the question isn’t “Can I avoid this dependency?”, but “Am I willing to pay the price when the time comes?”.
Because the time will come. Maybe in a month, maybe in a year, maybe in five. But it will come. And when it comes, it will be just you, your code, and a bug in the middle of thousands of lines written by someone else.
In that moment, you’ll understand the true cost of dependencies. It’s not the megabytes of disk space they occupy or the extra milliseconds of compilation time. It’s the loss of control. It’s the delegation of responsibility. It’s technical debt disguised as convenience.
Every time you add a dependency, you’re making a bet: you’re betting that the time you save now is worth more than the potential future pain. Sometimes it’s a good bet. Often it’s not.
The zero-dependency project is useless extremism. But the project that pulls in all of Crates.io “because it’s free anyway” is equally insane.
Virtue lies in the conscious middle ground. In recognizing that every dependency is a Faustian bargain, and in deliberately deciding which bargains we’re willing to make.
Because in the end, when your software explodes (and it will, it’s only a matter of time), the question won’t be “Whose fault is it?”. The question will be “Did you know what you were importing?”.
And if the answer is no, maybe the problem wasn’t the dependency.
It was you.
P.S.: After writing this article, I checked the dependencies of my latest project. 6 direct, 202 transitive. I don’t even know what half of them do. Maybe I should start practicing what I preach. Or maybe not. After all, what could possibly go wrong?
Note: This article was translated using AI.
Commenti