Ubin.io All Articles
Engineering Culture

You Don't Have a Dependency Problem. You Have a Boundaries Problem.

By Ubin.io Engineering Culture
You Don't Have a Dependency Problem. You Have a Boundaries Problem.

Every developer has been there. You run npm install and watch the terminal scroll for what feels like a geological epoch. You audit your Python requirements and find seventeen packages you've never heard of, two of which have open CVEs. You try to upgrade a library and spend three days untangling version conflicts that cascade through your entire dependency graph like a bad game of Jenga.

The instinct is to reach for dependency management tooling. Lock files, vulnerability scanners, private registries, vendoring strategies. Those tools matter. But if you're fighting dependency hell on a regular basis, the tooling isn't where your problem lives.

Your problem is that your component boundaries are fuzzy. And fuzzy boundaries are a dependency magnet.

What Boundaries Actually Do

A component boundary is a deliberate decision about what a piece of your system is responsible for — and equally important, what it isn't responsible for. When boundaries are clear, dependencies follow naturally. When they're unclear, dependencies accumulate like stuff in a junk drawer.

Think about it this way: a component with a well-defined purpose only needs libraries that serve that purpose. A component that's doing four things — because nobody ever decided where those four things should live — needs libraries for all four of them. And when those libraries have their own transitive dependencies, and those libraries have their transitive dependencies, you end up with a dependency graph that looks like a bowl of spaghetti.

The node_modules folder that weighs more than your production database isn't a JavaScript problem. It's a design problem wearing a JavaScript costume.

The Ownership Question Nobody Asks

Here's a diagnostic worth running on your own codebase: for every third-party library you're pulling in, ask why that library exists in this component.

Sometimes the answer is clean. "We use this PDF generation library because this service is responsible for generating PDFs." Great. Clear ownership, clear dependency.

But often the answer is murkier. "We use this date formatting library because... we needed to format some dates when we were building this endpoint." Okay, but is date formatting a core responsibility of this component? Or did it land here because the component's actual responsibility was never defined tightly enough to push that logic somewhere more appropriate?

Libraries that don't obviously belong to a component's core purpose are usually symptoms. They're telling you that something crept in — some functionality that doesn't have a clear home, so it found a convenient one. Enough of those and you've got a component that's doing too much, depending on too much, and breaking in unexpected ways when any of those dependencies shift.

Transitive Dependencies and the Illusion of Control

The supply chain security conversation in the developer community has gotten loud lately, and for good reason. The colors.js incident, the node-ipc situation, the endless stream of malicious packages slipping into the npm registry — these are real risks.

But here's what often gets lost in that conversation: you can't meaningfully audit dependencies you didn't intentionally choose. When your direct dependency count is high, your transitive dependency count is stratospheric. At that scale, automated scanning becomes a fire hose of alerts and the signal gets buried in noise.

Intentional dependency design — choosing fewer, more focused libraries because your components have clearer responsibilities — is also a security posture. Smaller dependency graphs are auditable. They're understandable. When something goes wrong, you can reason about the blast radius.

Designing Cleaner Interfaces

So how do you actually tighten the boundaries? A few patterns that work in practice:

Define your component's contract explicitly. Before you add a library, write down in plain language what this component does and doesn't do. If the library's purpose doesn't map directly to that definition, it probably doesn't belong here — or the definition needs to change first.

Prefer thin adapters over direct library usage. Instead of calling a third-party HTTP client directly throughout your codebase, wrap it in a thin adapter that exposes only the interface your code needs. Now the library is behind a boundary. Swapping it out later doesn't require hunting through 40 call sites.

Push shared utilities to explicit shared modules. If the same helper logic is getting duplicated across components because it doesn't "belong" to any one of them, that's a signal that you need a deliberate shared module — not that every component should re-import the same library independently.

Treat version conflicts as boundary violations. When two parts of your system need incompatible versions of the same library, that's often a sign that they're trying to own the same responsibility. The conflict is the codebase telling you something about your architecture.

When the Library Is the Problem

Sometimes a library genuinely is too heavy for what you need. A utility package that pulls in 40 transitive dependencies to give you one helper function is a reasonable thing to replace with 10 lines of code.

But be honest with yourself about the distinction. "This library is too heavy" and "I don't understand why we're using this library" are different problems. The first might justify replacement. The second justifies a conversation about ownership.

And before you write your own version of something, make sure the real motivation isn't just preference. Rolling your own cryptography, your own HTTP client, your own auth layer — these carry risks that usually outweigh the dependency cost. The goal is cleaner boundaries, not fewer dependencies at any cost.

The Payoff

Teams that invest in boundary design don't talk about dependency hell that often. Not because they've found magical tooling, but because their components are small enough and focused enough that the dependency graph stays manageable almost automatically.

When each piece of your system knows exactly what it's responsible for, the libraries it needs become obvious. The ones it doesn't need become obviously out of place. And the whole system becomes easier to audit, easier to upgrade, and easier for the next engineer to understand without a guided tour.

Clean dependencies are a byproduct of clear thinking about what your code actually does. Start there.