Koragraph

The dependency graph

A dependency graph records which units of code rely on which others through imports and packages, at the coarser grain of files and modules rather than individual calls.

A dependency graph records which units of code rely on which other units. The units are files, modules, and packages, and one relies on another when it imports or otherwise pulls in what the other provides. It is the same idea as the call graph but at a coarser grain: it tracks whole files and modules depending on each other rather than individual functions calling each other.

Imports, modules, and packages

Code is organized in nested containers. The smallest unit most people work in is a file. Files are grouped into modules, which are self contained units of related code that expose some of what they define for others to use. Modules are gathered into packages, which are units you can publish, install, and version as a whole. The exact words differ between languages, but the layering is nearly universal: files inside modules inside packages.

These units rely on each other through imports. An import is a line near the top of a file that says, in effect, this file needs what that other module provides, so make it available here. When file A imports module B, A cannot do its job without B. That reliance is a dependency, and it is a directed relationship: A depends on B, and not, by virtue of this import, the other way around. Record every such reliance as a directed edge from the dependent unit to the unit it depends on, with the units as nodes, and you have the dependency graph.

A coarser grain than calls

The dependency graph and the call graph describe the same codebase, and the difference between them is grain, meaning the size of the thing each node stands for. This is the idea to hold on to, because it is what makes the two graphs answer different questions.

In the call graph, a node is a single function and an edge is one function calling another. It is fine grained. It can tell you that this exact function reaches that exact helper. In the dependency graph, a node is a whole file or module and an edge is one file depending on another, usually because something in the first imports something in the second. It is coarse grained. It does not care which function called which. It cares that this file, as a whole, needs that module, as a whole.

A quick analogy. The call graph is a map of who phoned whom, person by person. The dependency graph is a map of which departments do business with which other departments. The call level map is more detailed, and you want it when your question is about specific interactions. The department level map is simpler and shows the shape of the organization at a glance, and you want it when your question is about structure. Neither is more correct. They answer questions at different sizes, and part of using them well is knowing which size your question is.

The call graph asks which function calls which. The dependency graph asks which file needs which. Same code, different grain, and the coarser view is often the one that shows the architecture.

Direction and layering

Dependency edges point, and the direction carries the meaning: the arrow runs from the unit that needs, to the unit that is needed. A file that imports a logging module points at the logging module. The logging module, if it is well designed, points at nothing high level; it just does its narrow job. This direction is what lets a dependency graph reveal the layering of a system.

Most healthy codebases are layered. High level code, the part that expresses what the application does, sits on top. It depends on mid level code, general building blocks like data handling and business rules. That in turn depends on low level code, the small utilities that everything uses. Drawn as a dependency graph, the arrows all tend to point the same way: from the high level down toward the low level. The utilities at the bottom depend on almost nothing. The application at the top depends, eventually, on nearly everything.

This downward flow is not decoration. It is what keeps a large system understandable. When dependencies point consistently from higher to lower, you can read and change a low level utility without worrying about the application code above it, because the utility does not depend on that code and cannot be surprised by it. The dependency graph is where you can actually see whether a codebase respects its own layers or whether the arrows have started pointing back upward in ways that tangle it.

Cycles, and why they hurt

The tangling has a specific and important form: the cycle. A cycle is when you can follow the dependency arrows and end up back where you started. Module A depends on module B, and B depends on A. Or the loop is longer: A depends on B, B on C, and C back on A. Either way the arrows form a ring.

Cycles hurt for a plain reason. If A needs B and B needs A, then you cannot fully understand, build, test, or change either one in isolation, because each drags in the other, which drags in the first again. The two units have effectively fused into one larger unit that merely looks like two. A change to A can ripple into B and back into A. A newcomer trying to learn A is forced to learn B at the same time. What was meant to be a clean separation has quietly become a knot.

In small doses a cycle between two closely related modules may be tolerable. But cycles tend to grow, pulling more modules into the ring, until a chunk of the codebase becomes one tightly wound mass that cannot be touched in pieces. A dependency graph makes cycles visible as literal loops in the arrows, which is often the first time anyone can see a problem that was always there but never drawn. Finding and breaking cycles is one of the most common uses of the graph.

Transitive dependencies

A dependency is rarely just one hop. If your file depends on a module, and that module depends on three others, and each of those depends on more, then your file depends, transitively, on all of them. Transitive simply means carried through the chain: the dependencies of your dependencies are also, in a real sense, your dependencies, because if any of them breaks, the chain that reaches you breaks too.

This is why a project that directly imports a handful of packages can end up with hundreds installed. Each package you name brings its own dependencies, which bring theirs, and the full set is the transitive closure: everything reachable by walking the dependency edges outward from what you directly asked for. The direct list is short. The real list, the one that has to be present for your code to run, is the whole reachable set, and you find it with the same forward walk over edges that every graph uses.

Transitive dependencies are where a lot of real risk lives, precisely because they are not in front of you. You chose the packages you import directly. You did not choose the dozens they pulled in, and yet a flaw deep in that transitive set can reach all the way up to your code. The dependency graph is what turns that hidden reach into something you can actually see and walk.

Transitive reach also explains a kind of change that surprises people: a small edit in a low level module that ripples out to a large part of the system. Because the low level module sits near the bottom of the layering, a great many units depend on it transitively, even though few name it directly. Walk the dependency edges backward from that module, following the arrows against their direction, and you gather everything that rests on it. The set can be large, and that is not a flaw in the graph. It is the truth about how load bearing the module is, made visible before you touch it rather than discovered afterward when something distant breaks.

Build order falls out of the graph

There is a practical payoff that comes almost for free once you have the dependency graph: knowing the order to build things in. If module A depends on module B, then B has to be built before A, because A cannot be assembled until the thing it needs exists. Turn that around across the whole graph and the correct build order is just the units arranged so that everything comes after the things it depends on.

Working out such an order is a standard operation on a directed graph. You can always find one, with a single firm condition: there must be no cycles. If A depends on B and B depends on A, there is no order that puts each after the other, because that is a contradiction. This is a second, concrete reason cycles hurt. They do not just tangle understanding; they make a clean build order impossible, and build tools will either refuse or resort to awkward workarounds. The same walk that gives you build order also gives you safe orders for testing and for rolling out changes, since in each case you want to handle the depended upon things before the things that depend on them.

A dependency graph without cycles can always be put in an order where everything comes after what it needs. Add one cycle and that order stops existing. This is why cycles are worth hunting.

How it differs from and completes the call graph

The dependency graph and the call graph are not rivals. They are two views of one codebase that answer different questions, and you want both. It is worth being precise about when each is the right tool.

Reach for the dependency graph when your question is about structure and organization. Is this codebase cleanly layered, or have the arrows started pointing the wrong way? Are there cycles that have fused modules together? If I depend on this package, what does that really drag in? What order must these units be built in? These are all questions about whole units and how they are arranged, and the coarse grain is exactly what makes the answers readable, because it hides the individual calls that would drown out the shape.

Reach for the call graph when your question is about specific behavior and impact. If I change this one function, which other functions are affected? What exactly does this function depend on to run? Those need the fine grain of individual functions, because a dependency edge between two files only tells you the files are connected, not which function inside one reaches which function inside the other. The dependency graph would say the two files are related; the call graph tells you precisely how.

The two even relate to each other in a clean way. If any function in file A calls any function in file B, then file A depends on file B. So the dependency graph is, in part, the call graph zoomed out: collapse every function into the file it lives in, merge the call edges that now run between the same pair of files, and much of the dependency graph appears. The dependency graph adds relationships that are not calls at all, such as importing a module only for its type definitions or its constants. That is why you keep both. The zoomed out view shows the architecture. The zoomed in view shows the consequences.

Where this connects

The dependency graph is one slice of the larger code knowledge graph, the slice whose edges are imports and package relationships, and it sits beside the other slices that make up that whole. The finer grained companion that tracks individual functions calling each other is the call graph, and reading the two together is how you move between architecture and impact. The units the dependency graph is built from, the files and modules and how they are arranged, are the subject of what a codebase actually is. And the relationships that reach across separately running programs, which behave like dependencies but cannot be seen by looking inside a single repository, are cross-service edges. Each of those is a short walk from here.

Connected concepts

The code knowledge graphA code knowledge graph is a structured map of a codebase where every declaration is a node and every real relationship, calls, imports, inheritance, cross-service links, is an edge.The call graphA call graph records which functions call which, so that following the edges forward gives everything a function depends on and backward gives everything that depends on it.What a codebase actually isA codebase is a large pile of interdependent text files that together describe a running system, most of whose meaning lives in the links between them rather than in any one file.Cross-service edgesA cross-service edge is a dependency that leaves one repository or service and lands in another, which is exactly where single-repository tooling goes silent.What a graph isA graph is nothing more than a set of things and the connections between them, and it is the natural shape for anything whose meaning is relational.

Where this sits

Back to the full graphThe short glossary