The Dependency You Didn't Choose
There’s a particular kind of bug that only shows up when you update something you didn’t write.
You pull in a library because it solves a real problem. Date parsing, HTTP handling, YAML deserialization — the kind of thing that’s tedious to implement correctly and dangerous to get wrong. The library works. You move on. Months pass.
Then a new version drops, and your build breaks.
The Hidden Contract
Every dependency is a contract. Not the API contract — that’s the part you can read. The hidden contract is everything else: the assumptions about memory allocation, the threading model, the error handling philosophy, the way edge cases are resolved.
When you call a function that returns a pointer, is it your responsibility to free it? The documentation might say. Or it might not, and you’re left reading source code to figure out whether you’re holding a borrowed reference or an owned one.
In GLib, this is explicit. Functions document transfer semantics — transfer full, transfer none, transfer container. It’s verbose, but it means you can read the contract without reading the implementation. Most libraries aren’t that disciplined.
The Compound Effect
One dependency is manageable. Ten is a system. Each one brings its own assumptions about how the world works, and those assumptions interact in ways nobody designed.
Library A assumes UTF-8 everywhere. Library B handles encoding internally and might hand you Latin-1. Library C doesn’t care about encoding at all — it’s just bytes. Individually, each is fine. Together, you get mojibake in production and a three-hour debugging session that ends with a single g_utf8_validate() call.
The compound effect is why experienced developers get nervous about dependency counts. It’s not about the code quality of any individual library. It’s about the surface area of assumptions you’re implicitly accepting.
The Vendoring Question
One response is to vendor everything. Copy the source into your project, pin it forever, never update unless you choose to. This trades update risk for staleness risk — you won’t get broken by upstream changes, but you also won’t get security patches.
Another response is minimal dependencies. Write the boring parts yourself. A YAML parser is a significant undertaking, but a configuration file reader that handles the subset of YAML you actually use? That’s an afternoon.
The right answer depends on what you’re building. A prototype that needs to ship this week should pull in everything that accelerates development. A system that needs to run for years should be ruthless about what it depends on.
The Submodule Approach
There’s a middle path that works well for C projects: git submodules with source-level inclusion. You get the exact version you tested against, compiled with your flags, linked into your binary. Updates are explicit — you bump the submodule, rebuild, run tests, and know immediately if something broke.
It’s more work than a package manager. It’s also more control. When your build breaks, you can git diff the submodule and see exactly what changed. When a function signature shifts, you find out at compile time, not at runtime in production.
The Real Cost
The cost of a dependency isn’t measured in lines of code or binary size. It’s measured in the assumptions you inherit and the debugging sessions you’ll eventually have when those assumptions conflict with yours.
Every library you add is a bet that its maintainers’ priorities will continue to align with yours. Sometimes that bet pays off for decades. Sometimes it doesn’t survive a minor version bump.
Choose carefully. And when something breaks after an update you didn’t expect to matter — check the assumptions, not just the API.