What Is a Code Dependency Graph? A Developer’s Guide (Updated 10JUN26)
Learn what code dependency graphs are, how to read nodes and edges, and how dependency analysis helps developers understand architecture, coupling, cycles, refactoring risk, and onboarding.
You open a large codebase for the first time.
Not a toy project — a real one. Hundreds of files. Deep directory trees. Familiar names mixed with things you have never seen before. You click into one file, follow an import, then another, then another. Ten minutes later you are three directories deep and cannot remember how you got there.
You are not lost because the code is necessarily bad.
You are lost because the structure of the system is invisible.
Every codebase has an architecture, whether it was carefully designed or slowly accumulated over years. But that architecture usually exists implicitly, scattered across import statements, package files, framework conventions, and build configuration. Your brain is expected to reconstruct it on the fly.
That works until it does not.
The moment you start asking questions like these, you are asking dependency-graph questions:
- What breaks if I change this file?
- Which modules depend on this utility?
- Why is this service hard to test?
- Where does the core logic actually live?
- Which files form a circular dependency?
- What part of the system should I learn first?
A dependency graph is how you stop guessing.
It turns the invisible structure of your codebase into something you can inspect.
What Is a Dependency Graph?
A dependency graph is a directed graph that shows how parts of a system depend on each other.
In a code dependency graph:
- Nodes represent units of code, such as files, modules, packages, or directories.
- Edges represent dependency relationships, such as imports, requires, includes, or references.
- Direction shows which unit depends on which other unit.
The simplest form looks like this:
A → B
That means:
A depends on B
or:
A cannot function without B
That single arrow carries a lot of architectural information. If B changes, A may be affected. If many files point to B, B has a wide blast radius. If arrows loop back on themselves, the system contains a cycle.
You already think this way informally. Every time you scan imports at the top of a file, you are asking:
What does this file rely on?
Every time you search for usages of a function or class, you are asking:
Who depends on this?
A dependency graph captures those relationships across the whole codebase instead of forcing you to reconstruct them one file at a time.
A Concrete Example
Imagine this code:
# auth/service.py
import database
import crypto
# billing/payment.py
import auth.service
import database
As dependency edges:
auth/service.py → database.py
auth/service.py → crypto.py
billing/payment.py → auth/service.py
billing/payment.py → database.py
From that graph, a few things are immediately visible:
database.pyis foundational because multiple files depend on it.auth/service.pysits in the middle because billing depends on it.crypto.pyis narrower because only auth depends on it.billing/payment.pyis higher-level because it depends on several lower-level pieces.
The code did not change. Your visibility changed.
That is the value of a dependency graph.
Code Dependency Graphs vs. Package Dependency Graphs
“Dependency graph” can mean several related things, so it helps to separate them.
Code Dependency Graph
A code dependency graph maps relationships inside your source code:
api/routes.py → services/user_service.py → models/user.py
This is the kind of graph developers use to understand architecture, refactoring risk, circular dependencies, coupling, onboarding paths, and module boundaries.
Package Dependency Graph
A package dependency graph maps third-party package relationships:
your app → Flask → Werkzeug
your app → React → scheduler
This is the kind of graph used by package managers, SBOM tools, and vulnerability scanners. It helps answer questions like:
- Which packages are installed?
- Which packages are transitive dependencies?
- Which vulnerable libraries affect my application?
Service Dependency Graph
A service dependency graph maps runtime service relationships:
frontend → api gateway → auth service → user database
This helps with distributed systems, reliability, observability, incident response, and service ownership.
These graphs overlap, but they are not identical. A source-code dependency graph may show where a vulnerable library is imported in your codebase, but package vulnerability management usually requires package-lock, SBOM, or scanner data. A service graph may show runtime calls that are not visible from static imports alone.
This article focuses mainly on code dependency graphs: the structure created by files, modules, imports, and internal relationships.
How to Read a Dependency Graph
Reading a dependency graph starts with five questions:
- Incoming edges: Who depends on this node?
- Outgoing edges: What does this node depend on?
- Cycles: Do any dependency paths loop back on themselves?
- Depth: How many layers does a change pass through?
- Clusters: Which parts of the system are tightly connected?
You do not need to inspect every edge. Start with structure.
A file with many incoming edges is load-bearing. A file with many outgoing edges has many prerequisites. A file with both is a coupling hotspot. A group of files with arrows looping around each other may be a circular dependency or strongly connected component.
The goal is not to make a pretty diagram. The goal is to answer architectural questions faster.
Why Dependencies Matter
Code does not exist in isolation.
Every import creates a relationship. Every relationship creates a potential path for change, risk, and complexity.
When you write:
from app.auth import verify_token
you create a dependency on app.auth. Your module now assumes that verify_token exists, behaves as expected, and remains available.
That dependency may be fine. Most dependencies are necessary.
The problem is that dependency structure accumulates. One import is easy to reason about. Hundreds or thousands of imports across a repository create an architecture that may or may not match what the team thinks it built.
Dependency graphs help reveal that architecture.
What Dependency Graphs Reveal
Change Blast Radius
If you modify a file imported by 30 others, the potential blast radius is wider than if you modify a file imported by one.
That does not mean the change will break all 30 dependents. Dependency count is not a perfect risk score. But it is a strong signal that the file is load-bearing and deserves more careful testing.
A dependency graph lets you ask:
What depends on this file?
before you make the change.
Coupling Hotspots
Some files import many modules and are imported by many modules.
Example:
auth/service.py
outgoing edges: 12
incoming edges: 23
That file both needs a lot of context and affects a lot of other code. It is a pressure point.
Coupling hotspots are often where refactors get difficult, tests get complicated, and onboarding slows down.
Architectural Zones
In a healthy system, clusters often align with intended architecture:
routes/
services/
domain/
infrastructure/
A dependency graph shows whether those zones are real or only implied by folder names.
You may intend:
routes → services → domain → infrastructure
but discover:
domain → routes
infrastructure → services
services ↔ services
The graph exposes the difference between architecture-as-designed and architecture-as-implemented.
Circular Dependencies
A circular dependency exists when a dependency chain loops back to the starting point:
auth.py → user.py → auth.py
Circular dependencies make modules harder to understand, test, and refactor. In some languages they also cause runtime import failures.
A dependency graph makes cycles visible. More importantly, it can show whether a cycle is isolated or part of a larger strongly connected component.
For a deeper guide, see What Are Circular Dependencies? Cycles, SCCs, and How to Fix Them.
Orphaned or Dead Code
A node with no incoming edges may be unused:
old_payments.py ← no incoming edges
That does not automatically mean the file is safe to delete. It might be an entry point, script, plugin, migration, or dynamically loaded module. But it is worth investigating.
Dependency graphs are useful for dead-code discovery because they quickly surface files that nothing else appears to reference.
Onboarding Paths
New developers learn codebases by following dependencies.
A dependency graph can show which entry points lead to simple paths and which lead directly into high-coupling areas.
A good onboarding path might look like this:
api/products.py → services/product_service.py → models/product.py
A rough first assignment might look like this:
api/orders.py → services/order_service.py → legacy/workflow.py → billing/payment.py → auth/session.py
Both may be valid paths through the system, but they are very different onboarding experiences.
Anatomy of a Code Dependency Graph
Nodes
Nodes represent the units being analyzed.
Common node types include:
- files
- modules
- packages
- directories
- classes or functions, in more granular graphs
File-level graphs are often the most practical starting point because they align with how developers navigate code.
Edges
Edges represent relationships between nodes.
Common edge types include:
importrequireincludeexport- package dependency
- type reference
- framework registration
- generated or inferred dependency
The exact edge types depend on the language and analyzer.
Direction
Direction is critical.
A → B
means A depends on B.
This also means risk often flows in the opposite direction of the arrow. If B changes, A may be affected. If many things point to B, then B is foundational.
That is why incoming edges matter so much.
Fan-In
Fan-in is the number of incoming dependencies.
many files → config/settings.py
High fan-in means many files depend on this node. These nodes are often stable abstractions, shared utilities, models, configuration files, or central services.
High fan-in can be good or bad. A stable, well-tested shared module may naturally have high fan-in. A messy utility file with high fan-in may be a god module.
Fan-Out
Fan-out is the number of outgoing dependencies.
billing/payment.py → many files
High fan-out means this node depends on many other modules. These nodes are often harder to understand because they require more context.
High fan-out can signal orchestration code, feature creep, or poor separation of responsibility.
Depth
Dependency depth shows how many layers of relationships exist between an entry point and a lower-level dependency:
app.py → auth/service.py → database/models.py → database/connection.py → postgres_driver.py
Deep chains are not automatically wrong, but they increase the number of concepts a developer must follow to understand behavior.
Cycles and Strongly Connected Components
A cycle occurs when the graph loops back:
A → B → C → A
A strongly connected component is a group of nodes where every node can reach every other node.
SCCs are important because they reveal the real unit of coupling. Five files in one SCC may behave like one tangled module even if the directory tree makes them look separate.
Worked Example: Reading a Small Graph
Suppose a graph shows this:
auth/service.py
imported by:
api/routes.py
billing/webhooks.py
admin/dashboard.py
imports:
database/models.py
utils/crypto.py
config/settings.py
database/models.py
imported by:
47 files
imports:
database/connection.py
utils/crypto.py
imported by:
auth/service.py
imports:
none
What can you infer?
First, database/models.py is foundational. Forty-seven files depend on it, so changes deserve careful testing and review.
Second, auth/service.py is moderately coupled. It imports several lower-level modules and is used by three higher-level areas. It is not isolated, but the blast radius is understandable.
Third, utils/crypto.py is narrow. It has one known dependent and no internal dependencies. It is likely easier to test and refactor.
Fourth, if you modify authentication behavior, you already know where to look:
api/routes.py
billing/webhooks.py
admin/dashboard.py
Without the graph, you would need to search manually and hope you found all usages.
Common Dependency Problems
Circular Dependencies
A circular dependency looks like this:
auth/service.py → user/models.py → auth/service.py
This means the modules cannot be understood or loaded independently. In Python, it can produce partially initialized module errors. In JavaScript and TypeScript, it can produce initialization-order issues or surprising runtime behavior.
The common fix is to remove one edge from the loop:
Before:
auth/service.py ↔ user/models.py
After:
auth/service.py → user/models.py
auth/service.py → security/passwords.py
user/models.py → security/passwords.py
The shared password logic moves to a neutral module. Both sides depend on it, but not on each other.
For detailed strategies, see What Are Circular Dependencies? Cycles, SCCs, and How to Fix Them.
God Modules
A god module is imported by many parts of the system and contains too many unrelated responsibilities.
utils.py
imported by: 67 files
contains: dates, strings, user helpers, billing helpers, email helpers
The problem is not high fan-in alone. Some central modules should be widely used. The problem is broad usage combined with unclear responsibility.
A better structure splits by purpose:
utils/dates.py
utils/strings.py
billing/formatting.py
users/display.py
Now each module has a narrower role and a smaller blast radius.
Tight Coupling
A tightly coupled module imports many unrelated pieces:
from auth.service import authenticate
from user.models import get_user
from payment.processor import charge_card
from email.service import send_receipt
from database.session import session
from cache.redis import get_cached
from integrations.stripe_client import Stripe
from tax.calculator import calculate_tax
Each import may have been added for a good reason. Together, they make the file hard to test, hard to understand, and hard to reuse.
The solution is usually not “remove imports blindly.” It is to ask whether the module is doing too much. Some code should orchestrate. Some code should compute. Some code should adapt external systems. Mixing all of those roles creates coupling.
Deep Dependency Chains
Deep chains look like this:
UI → Controller → Service → Repository → ORM → Database → Driver
That may be intentional architecture. But if every simple feature requires following seven layers, the design may be adding more indirection than clarity.
Depth becomes a problem when developers cannot understand behavior without chasing a long chain of thin wrappers.
Orphaned Modules
Orphaned modules have no incoming edges:
legacy_auth.py
old_payments.py
deprecated/importer.py
Some are legitimate entry points or scripts. Others are dead code. The graph does not delete them for you, but it gives you a short list to investigate.
Layer Violations
If the intended architecture is:
API → Services → Domain → Infrastructure
then this is suspicious:
Domain → API
Infrastructure → Services
Models → Routes
Layer violations are often more important than raw dependency counts because they show dependency direction breaking down.
How to Generate a Dependency Graph
You can generate dependency graphs several ways.
Manual Inspection
For small projects, you can inspect imports manually:
- Open a file.
- List its imports.
- Search who imports it.
- Repeat.
This works for small examples. It does not scale well.
Manual inspection becomes unreliable when a repository has hundreds of files, indirect imports, generated code, barrel files, or multiple languages.
IDE Features
Most IDEs provide some dependency visibility:
- find usages
- go to definition
- call hierarchy
- package or module dependency views
- import-cycle warnings in some languages
These tools are useful while working in one area. They are less useful when you need a repository-wide architectural view.
Language-Specific CLI Tools
Language-specific tools can generate dependency graphs or detect cycles.
For Python:
pydeps mypackage --show-cycles
For JavaScript and TypeScript:
madge --circular src/
For Go:
go list -deps ./...
These tools are valuable because they fit naturally into local workflows and CI. The tradeoff is that each tool is usually language-specific and may not give a unified view of a polyglot repository.
Automated Repository Analysis
Automated dependency analyzers inspect a repository, build a graph, and summarize structural signals.
Useful outputs include:
- nodes and edges
- internal vs external dependencies
- fan-in and fan-out
- circular dependencies
- strongly connected components
- dependency depth
- language breakdown
- high-coupling files
- repository-level summaries
- downloadable or queryable analysis artifacts
PViz is one implementation of this approach. The hosted app can generate dependency analysis for supported repositories and languages.
The important capability is not just a visual graph. It is the ability to ask questions like:
- What depends on this file?
- What does this file depend on?
- Where are the cycles?
- Which modules are high fan-in?
- Which modules are high fan-out?
- Which files belong to the largest SCC?
Those questions are what make the graph useful.
Using Dependency Graphs Effectively
Before Refactoring
Suppose you need to change authentication behavior.
Without a graph, you edit auth/service.py, run the tests you know about, and hope you covered the impact.
With a graph, you first inspect incoming edges:
auth/service.py is imported by:
api/routes.py
billing/webhooks.py
admin/dashboard.py
jobs/worker.py
middleware/auth.py
Now you have a test plan. You know where the behavior is used.
During Code Review
A pull request adds this import:
# models/user.py
from external_api import fetch_user_permissions
The graph shows:
models/user.py is imported by 40 files
That means the PR may have added an external API dependency to a load-bearing model. Maybe that is acceptable. Maybe it should happen in a service layer instead. Either way, the graph turns an architectural concern into something visible during review.
During Onboarding
A new developer needs to understand the system.
Instead of saying:
Just start reading files and ask questions.
you can show:
entry points → services → domain → infrastructure
You can identify:
- foundational modules
- safe starting points
- high-risk areas
- circular dependency clusters
- legacy modules to avoid initially
This turns onboarding from a scavenger hunt into a guided path.
For a more detailed approach, see How to Measure Codebase Complexity for Developer Onboarding.
During Architecture Planning
If you are splitting a monolith, dependency graphs help identify natural boundaries.
Look for:
- clusters with many internal dependencies
- few cross-cluster dependencies
- shared modules that prevent separation
- cycles across intended service boundaries
For example:
auth/ and user/ are tightly connected
billing/ and payment/ are tightly connected
auth/ and billing/ barely interact
That suggests possible service or package boundaries.
The graph does not make the architecture decision for you, but it shows the coupling you must account for.
During Security and Risk Review
Package dependency graphs and SBOM tools are the primary tools for vulnerability management. Source dependency graphs provide a complementary view: they show where vulnerable libraries or risky internal modules are used in your codebase.
For example:
vulnerable_client.py is imported by:
auth/oauth.py
api/external.py
and transitively affects:
auth/oauth.py → middleware/auth.py
api/external.py → jobs/sync_worker.py
Now the team can prioritize testing and remediation based on actual usage paths, not only package presence.
What to Do When the Graph Looks Bad
The first graph of a mature codebase may look messy.
That is normal.
Do not try to fix everything at once. Use the graph to prioritize.
Start With One Problem
Pick one:
- one circular dependency
- one high fan-out module
- one god module
- one layer violation
- one confusing onboarding path
Fix it. Regenerate the graph. See what changed.
Define Intended Boundaries
Write down the intended direction:
routes → services → domain → infrastructure
Then compare the graph to that direction.
When the graph disagrees with the intended architecture, the disagreement is the work.
Use the Graph in Code Review
Before merging a structural change, ask:
- Does this add an import in the wrong direction?
- Does this increase fan-out in an already complex module?
- Does this create a cycle?
- Does this move behavior to a better boundary?
- Does this make the graph easier or harder to explain?
You may still merge a tradeoff. Deadlines exist. But the cost becomes visible.
Track Improvement Over Time
Dependency graphs are most useful when generated repeatedly.
Track:
- number of cycles
- size of the largest SCC
- high fan-in modules
- high fan-out modules
- layer violations
- dependency depth from common entry points
The goal is not to make the graph perfectly clean. The goal is to make structural debt visible enough to manage.
Conclusion
A code dependency graph is an X-ray of your codebase.
It shows:
- what depends on what
- where complexity lives
- where change is risky
- where cycles and SCCs exist
- where architecture matches reality
- where technical debt hides
Without a dependency graph, you navigate structure indirectly. You read files one by one, follow imports manually, and hope the architecture emerges.
With a dependency graph, you see the system at once.
The code does not change.
Your visibility does.
And that changes how you refactor, review, onboard, debug, and plan architecture.
If you want to generate that visibility, start with a source dependency graph for your repository. Use it to answer one practical question:
What depends on this?
Then use the next question:
What does this depend on?
Those two questions are often enough to reveal where the system is stable, where it is fragile, and where the architecture needs attention.
Generate a Dependency Graph for Your Codebase
PViz can generate dependency analysis for supported repositories and languages, including dependency relationships, coupling signals, circular dependencies, and strongly connected components. The hosted app is useful for architectural review for supported codebases.
Related Reading
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.