Dependency Graphs Were Never Just About Imports

A practical guide to the different kinds of software dependency graphs—from build and module graphs to call, data-flow, deployment, and change-coupling graphs—and the engineering questions each can actually answer.

12 min read
dependency-graphssoftware-architecturestatic-analysisbuild-systemssoftware-engineering

Why can two dependency graphs of the same codebase disagree—and both be correct?

Imagine asking two tools to map the same repository. One produces a clean set of package-to-package relationships. The other shows hundreds of function-level calls, including callbacks and framework entry points that never appeared in the first graph. A third analysis claims that two files are tightly connected even though neither imports the other.

It is tempting to conclude that at least one graph is wrong.

Sometimes that is true. Dependency analysis can miss relationships, invent false ones, reverse edge direction, or combine scopes that should remain separate. But disagreement alone is not evidence of failure. The tools may be modeling different kinds of dependency.

"Dependency graph" sounds like the name of one artifact. In practice, it describes a family of models. Build graphs, module graphs, call graphs, data-flow graphs, deployment graphs, and change-coupling graphs all connect software entities. They do not make the same claim, and they do not support the same engineering decision.

That distinction is the starting point for this field guide:

An edge means only what the graph's construction rules say it means.

An import edge, a runtime call, and a historical co-change relationship can all be called dependencies. Only one tells you that a module names another module in source. Only one attempts to describe execution. Only one comes from repository history. Treating them as interchangeable turns useful evidence into false certainty.

The problem: "dependency" is overloaded

When an engineer asks for "the dependency graph," the real request is usually hiding underneath:

  • What must be built first?
  • Which module relies on this package?
  • What code might execute from this entry point?
  • Where can this value flow?
  • Which service must be available before deployment?
  • What else tends to change when this file changes?

Those are different questions. A graph designed for one may be nearly useless for another.

A module graph can show that checkout/service.py imports pricing/discounts.py. It cannot, by itself, prove that a discount function runs for every checkout—or at all. A call graph may show a path from the checkout handler to a discount calculation, but it does not explain whether the architectural dependency is intentional. A change-coupling graph may show that the two files are frequently committed together, but it cannot tell you whether the relationship is causal, accidental, or a consequence of team workflow.

The first step in reading any dependency graph is therefore not looking at its shape. It is identifying its semantics:

  1. What does each node represent?
  2. What creates an edge?
  3. Which direction does the edge point?
  4. Which relationships are excluded?
  5. What confidence or evidence supports the edge?

The companion Reading the Graph series focuses on visual patterns such as stars, chains, hourglasses, meshes, and islands. This series asks a prior question: what claim is the graph making in the first place? The same visual star can mean a shared library with many importers, a function with many callers, a service with many consumers, or a file that changes alongside much of the repository. The pattern may look identical while the engineering implications differ substantially.

The graph model: six different kinds of dependency

There was no single moment when software engineering "invented" the dependency graph. Each model emerged where an existing coordination problem outgrew what one engineer could hold in their head.

Build graphs trace back to tools like Make in the 1970s, built to solve a narrow, painful problem: recompiling an entire program after every change was slow, and tracking what actually needed rebuilding by hand was error-prone. Module graphs owe more to the modular-programming movement of the same decade—Parnas's argument for decomposing systems around information hiding gave engineers a reason to ask which modules a module was allowed to know about, not just which ones it happened to import. Call graphs grew out of compiler research, where representing possible execution paths mattered for optimization long before anyone needed to visualize them; data-flow and control-flow graphs share that lineage. Service and deployment graphs are newer, arriving with distributed systems and later microservices, once "depends on" started meaning a network call instead of a source reference. Change-coupling graphs are newer still, drawing on repository-mining research that treated version-control history itself as a data source once that history became large enough to analyze at scale.

The shared mathematics—nodes, edges, traversal—is less important than the differing claims each model makes. Each graph encodes a particular view of the system, shaped by the problem it was built to solve.

Build dependencies

A build graph connects targets to their prerequisites. A generated binary may depend on object files; an object file may depend on source and header files; a container image may depend on compiled artifacts and configuration.

The graph supports decisions about:

  • build order,
  • incremental rebuilds,
  • parallel execution,
  • cache reuse,
  • and detection of undeclared or cyclic prerequisites.

A build edge does not necessarily describe source-level architecture. Two modules may have no direct import relationship yet still participate in the same packaging or generation pipeline. Conversely, a source import may not appear as a distinct build edge if the build system operates at a coarser target level.

Module and package dependencies

A module graph connects structural units such as files, namespaces, packages, crates, assemblies, or components. Edges commonly come from imports, includes, references, re-exports, or language-specific resolution rules.

This is the graph most people picture when they hear "code dependency graph." It helps reveal layering, fan-in, fan-out, central modules, isolated regions, cycles, and boundary violations.

Its strength is architectural orientation. Its weakness is that structural availability is not execution. Importing a module says that one unit can refer to another under the language's rules. It does not prove which symbol is used, which branch runs, or which runtime implementation is selected.

Call graphs

A call graph connects callers to callees. Depending on the analysis, nodes may represent functions, methods, procedures, or call sites. Edges may come from static source analysis, runtime traces, or a combination of both.

Call graphs support questions about execution paths, reachability from entry points, recursion, dead-code candidates, security-sensitive flows, and the possible downstream effects of changing behavior.

They are also harder to construct reliably. Dynamic dispatch, callbacks, reflection, dependency injection, metaprogramming, generated code, and runtime configuration can make the real target uncertain. A static graph may include calls that never occur. A dynamic graph may omit valid paths that simply were not exercised during observation.

Data-flow and control-flow graphs

Control-flow graphs model how execution can move between basic blocks or statements inside a function or program. Data-flow graphs model how values, definitions, or states propagate through operations.

These graphs support more local and semantic questions:

  • Can execution reach this branch?
  • Where can untrusted input travel?
  • Which definitions can reach this use?
  • Is a value sanitized before it reaches a sink?
  • Which paths leave a resource unclosed?

Although they are often grouped with dependency analysis, their nodes and edges are very different from module graphs. A control-flow edge means "execution may continue here." A data-flow edge means "this value or definition may influence that operation." Neither should be casually interpreted as an architectural dependency between components.

Service and deployment dependencies

Modern systems extend beyond a repository. Service graphs connect deployable applications, APIs, queues, databases, caches, infrastructure resources, or external providers. Edges may represent network calls, message production and consumption, data access, startup prerequisites, or deployment relationships.

These graphs support operational decisions: rollout order, incident blast radius, ownership routing, availability planning, and migration sequencing.

They also introduce time and environment. A dependency may exist only in production, only for one tenant, only when a feature flag is enabled, or only during startup. A repository-level graph can miss these relationships entirely because the effective system is assembled through configuration and deployment.

Ownership and change-coupling graphs

Not every useful dependency is written in source. Repository-mining techniques can connect files or modules that are repeatedly changed together. Ownership graphs can connect code to teams or contributors. Issue and review history can reveal coordination boundaries that static structure does not show.

A strong co-change relationship may suggest a hidden architectural seam, duplicated business logic, a missing shared abstraction, or simply a recurring maintenance workflow. It is evidence of association, not proof of causation.

These graphs answer questions such as:

  • Which files should be reviewed together?
  • Where does knowledge concentrate?
  • Which apparently separate modules evolve as a unit?
  • Where might organizational boundaries conflict with technical ones?

They are especially valuable when the source graph looks clean but maintenance remains difficult.

How to traverse it: start with the neighborhood

Later posts in this series will use more specialized operations: topological ordering for builds, reverse reachability for change impact, cycle detection for architectural analysis, and path finding for call and data-flow investigations.

For this opening taxonomy, the common baseline is neighborhood inspection.

Choose a node and ask:

  • What are its direct outgoing neighbors?
  • What are its direct incoming neighbors?
  • What kind of edge connects each neighbor?
  • Is the relationship directed, weighted, scoped, or conditional?
  • What evidence created it?

This is intentionally modest. Direct neighbors do not reveal every transitive consequence, but they force the graph's semantics into the open before deeper traversal begins.

Suppose a file has twelve incoming edges. In a module graph, that may mean twelve importers depend on it. In a call graph, it may mean twelve call sites can invoke it. In a co-change graph, it may mean the file has historically moved with twelve others above some threshold. "High fan-in" is not a complete interpretation until the edge type is known.

The safest traversal rule is simple: name the relationship before following it.

A practical example: three edges between the same files

Consider two simplified Python files:

# checkout/service.py
from pricing.discounts import calculate_discount


def calculate_total(cart, customer):
    subtotal = sum(item.price for item in cart.items)
    discount = calculate_discount(cart, customer)
    return subtotal - discount
# pricing/discounts.py
def calculate_discount(cart, customer):
    # Promotion, customer tier, and eligibility rules
    ...

The same pair can be connected in at least three useful graphs.

In the module graph

There is an edge:

checkout/service.py -> pricing/discounts.py

Construction rule: checkout/service.py imports a symbol resolved to pricing/discounts.py.

Supported conclusion: the checkout module has a static source dependency on the pricing module.

Unsupported conclusion: every checkout invokes the discount logic.

In the call graph

There may be an edge:

calculate_total() -> calculate_discount()

Construction rule: the body of calculate_total contains a call resolved to calculate_discount.

Supported conclusion: the analysis found a possible call from one function to the other.

Unsupported conclusion: the call occurs in every runtime scenario, or that the statically resolved target is always the implementation executed in production.

In the change-coupling graph

There may be a weighted relationship:

checkout/service.py <-> pricing/discounts.py   weight: 0.72

Construction rule: the files appeared together in a defined proportion of relevant commits during the selected history window.

Supported conclusion: the files have often changed together under that measurement.

Unsupported conclusion: one file causes changes in the other, or that the relationship will continue.

The filenames are identical in all three views. The relationship is not.

Even edge direction changes meaning. In the import and call graphs, the arrow commonly points from consumer or caller to provider or callee. The co-change relationship may be undirected because the history does not establish dependency direction. A tool could reverse import edges to answer "what depends on this?" more efficiently, but then the graph must say so. Otherwise, a reader may mistake downstream consumers for upstream prerequisites.

This is why edge labels, provenance, scope, and direction are not decorative metadata. They determine what the graph can legitimately support.

What the graph cannot prove

No single dependency graph is an exhaustive model of a software system.

A build graph cannot prove that the architecture is well layered. A module graph cannot prove that imported code executes. A static call graph cannot prove which path production traffic takes. A dynamic trace cannot prove that unobserved paths are impossible. A data-flow graph cannot prove organizational ownership. A service graph cannot fully explain source-level coupling. A co-change graph cannot prove causation or design intent.

The limitation is not a reason to discard the graph. It is a reason to match the graph to the decision.

A useful dependency graph answers a narrow question with explicit rules and inspectable evidence. Problems begin when a narrow model is promoted into a universal explanation of the codebase.

Before acting on a graph, ask:

  • Is this the right graph type for the question?
  • Are the node boundaries appropriate?
  • Does the edge represent certainty, possibility, observation, or correlation?
  • Are production, test, generated, and external scopes separated?
  • What important relationships cannot be observed by this construction method?

Those questions turn the graph from a picture into an engineering instrument.

Choose the graph by the decision

Dependency graphs are valuable because they compress a large system into a structure that can be inspected and traversed. That compression always discards information. The goal is not to build the one true graph of a codebase. The goal is to build—or select—the graph whose omissions are acceptable for the decision in front of you.

If the decision is build order, use a build graph. If it is architectural layering, begin with modules and packages. If it is possible execution, inspect calls and control flow. If it is operational blast radius, include services and deployment configuration. If the question is why changes repeatedly spread across boundaries, repository history may reveal a relationship the source graph cannot.

The rest of The Dependency Graph Field Guide will take these models one at a time. We will examine build ordering, codebase shape, call-graph uncertainty, change-impact analysis, testing and release decisions, cycles and strongly connected components, unreliable edges, graph-guided engineering agents, and the construction principles that make a dependency graph trustworthy.

The recurring question will not be merely, "What does the graph show?"

It will be: What engineering decision is this graph qualified to support?

Try PViz on your own codebase

Get dependency graphs, coupling signals, and a compressed bundle ready for your LLM, for GitHub repositories, free with an account.