Reading the Graph: The Star Pattern and the God Module Problem
How to identify and decompose star-shaped dependency patterns before they become the single point of failure your entire codebase revolves around
When you visualize a dependency graph for the first time, most nodes look roughly similar — a handful of incoming edges, a handful of outgoing ones, embedded in a web of reasonable relationships.
Then you see it. One node with dozens of arrows pointing inward. Sometimes dozens pointing outward too. It sits at the center of the graph like a hub, everything else orbiting around it.
That's a Star. And it's one of the most common structural problems in production codebases.
What the Star Pattern Looks Like
In graph terms, a Star is a module with unusually high fan-in, fan-out, or both relative to the rest of the codebase.
Fan-in is the number of modules that import this one. A module with fan-in of 40 means 40 other modules depend on it — 40 places that will be affected by any change you make to it.
Fan-out is the number of modules this one imports. A module with fan-out of 30 means it has 30 direct dependencies — 30 concepts a developer needs to understand before they can fully reason about this file.
A pure fan-in Star is a heavily depended-upon module: foundational, load-bearing, dangerous to change. In a visual graph, it appears as a node with many arrows pointing into it — a crowd of dependents. In practice, it's often a domain model that outgrew its original scope, or a utility module that became the default destination for anything without a better home.
A pure fan-out Star is a module that reaches into everything: a coordinator, an orchestrator, or more often, a dumping ground. It appears as a node with many arrows pointing outward — a module that has absorbed awareness of an unreasonable portion of the system. High fan-out is sometimes architecturally intentional (an app factory, a dependency injection container) but is more often a sign that a module has accumulated responsibilities it was never designed to carry.
A module with both high fan-in and high fan-out is the worst case. It's coupled to almost everything in both directions. Changes to it propagate everywhere. Changes elsewhere break it. It's a structural knot — you can't untangle it without introducing change somewhere. In a graph visualization, this node appears visually dominant — edges radiating inward and outward in all directions, often positioned near the center of the layout because the layout algorithm has no choice but to put it there.
In structural terms, the Star is the simplest pattern to recognize precisely because it's so extreme. You don't need to study the graph carefully — the outlier is obvious. That's both its most useful property and its most dangerous one: it's easy to see once you're looking, and easy to ignore when you're not.
How Stars Form
Stars don't appear overnight. They accumulate through a series of individually reasonable decisions, usually over months or years, by developers who never intended to create a structural problem.
The Convenience Import
The most common origin story: someone needs a function that already exists somewhere. Rather than thinking carefully about where it should live architecturally, they import it from wherever it currently is. That file gains one more incoming edge. This happens dozens of times per sprint across a team of any size. No individual decision is wrong. The function existed, the import was legitimate, the PR passed review. Multiply this by hundreds of PRs over two years and you have a Star.
The underlying cause isn't laziness — it's the absence of a shared model of where things belong. When ownership is ambiguous, the path of least resistance wins. A module that already imports everything else is a natural destination for the next thing that needs a home, which is how Stars become self-reinforcing. The larger a Star gets, the more likely it is that whatever you need is already there.
The Shared Utility Trap
A developer creates a utils.py or helpers.ts to hold genuinely shared code. Early on, this is correct — a date formatting function belongs in one place, not scattered across every module that needs to format a date. But utilities are low-friction import targets. They're already imported by many modules, so adding one more use feels inconsequential. Over time, anything without an obvious home ends up there. The file grows from date formatting to string manipulation to HTTP configuration to whatever the last developer needed that didn't fit anywhere else. Its fan-in grows with it, because every module that needs any of those things now imports the whole file.
Over time, the module stops representing a concept and starts representing convenience.
The shared utility trap is particularly insidious because the early version of the module was correct. The problem isn't that someone created a utilities file — it's that utilities files have no natural boundary. There's no moment at which adding a new function to utils.py is obviously wrong. The pattern reveals itself only in aggregate, which is exactly when it's hardest to reverse.
The God Class
Object-oriented codebases often develop a class that accumulates methods because it's the logical owner of "most" things. UserManager, AppController, SystemService. Each new feature adds a method. Each method adds an import. The class becomes the hub through which everything passes.
God classes are often created by a senior developer early in a project's life, when the scope is small enough that a single class owning "user-related things" is reasonable. As the codebase grows, the class grows with it — not because anyone decided it should own everything, but because it already owned most things, and adding to it was always easier than creating something new. By the time the problem is visible, the class has accumulated enough methods that anyone proposing to break it up faces months of careful extraction work.
The Framework Entry Point
Sometimes the Star is architectural by design — a router, a middleware chain, an app factory. These legitimately need to import many things. The problem occurs when business logic starts leaking into the entry point, or when the entry point pattern gets copied into business logic where it doesn't belong.
Entry points are especially prone to this because they're the place where things get wired together, and wiring often involves knowing about many different modules. That's appropriate for initialization code. It becomes a problem when the entry point also starts making decisions — validating inputs, transforming data, enforcing business rules — that belong further down the stack. Once that drift starts, the entry point becomes a Star with legitimate architectural cover, which makes it harder to argue for decomposition.
Why Stars Are Dangerous
High Blast Radius
Every change to a Star is a high-risk change. With 40 fan-in modules, a bug you introduce will express itself in many different places — and the relationship between the change you made and the failure mode that surfaces may not be obvious. A behavioral change affects every consumer, each potentially relying on different, undocumented assumptions.
This isn't just a testing problem, though it is that too. It's a confidence problem. Developers learn quickly which modules are expensive to touch. Stars develop a reputation — informally, through the team's oral history — as modules where changes require extra care, extra review, extra testing time. Over time, teams start avoiding necessary changes to Stars not because the changes are technically difficult but because the cost of getting them wrong is disproportionately high. The Star becomes load-bearing not just structurally but psychologically.

A change to utils.py reaches every module in the first ring directly. Those modules then expose the second ring to downstream effects — a bug introduced at the hub can surface anywhere in this propagation zone.
Merge Conflicts
Stars attract changes because they're the natural home for anything without a clear owner. Multiple developers modifying the same file creates constant merge conflicts — not because of bad process, but because the file is doing too many jobs. When three developers are simultaneously adding features that each touch a different part of a God class, they're not collaborating on the same problem; they're accidentally working in the same file.
The merge conflict is a symptom, but the underlying problem is that the Star is the wrong unit of work. You can improve your branching strategy, your code review process, your PR size conventions — and you'll still have merge conflicts on a Star, because the module's scope guarantees that many different lines of work will intersect there.
Onboarding Friction
A high fan-out Star is one of the worst modules to assign to a new developer. Opening it reveals 30 imports — 30 other files they now need to understand before they can fully reason about this one. Each of those files may have its own fan-out. What looks like a bounded task ("understand this module") becomes an unbounded exploration of the dependency graph, because the Star connects to so much of the system.
High fan-in Stars are equally difficult from the other direction: a new developer trying to understand how a Star is used has to trace 40 call sites, each of which uses it differently, to build a complete picture of its behavior. Neither problem is unsolvable — experienced developers learn to work with Stars — but the learning tax is real, and it compounds across every new hire and every context switch.
Test Difficulty
Stars are hard to unit test in isolation. High fan-out means many dependencies to mock — a test suite for a Star with 30 fan-out requires setting up 30 dependency stubs before any test can run. High fan-in means many contexts to cover — to be confident that a change to the Star doesn't break any of its 40 consumers, you need tests that exercise each consumer's specific use case.
Teams often respond to this by skipping unit tests on Stars entirely and relying on integration tests instead. Integration tests are slower, harder to maintain, and less precise about what broke. A failing integration test tells you something is wrong somewhere in a chain of modules; a failing unit test tells you exactly what broke. Stars without unit tests are where problems surface last — often in production.
Identifying Stars in Your Codebase
The Visual Shape
In a dependency graph visualization, the Star is the easiest pattern to identify by sight. The hub node appears visually dominant — positioned near the center of the layout, with edges radiating in and out across the graph. When a graph has a Star, the layout algorithm tends to position it centrally because so many other nodes need to be near it. The rest of the graph arranges itself around the Star the way a city arranges itself around a highway interchange.
A fan-in Star looks like a convergence point — many arrows arriving from different parts of the graph, few leaving. A fan-out Star looks like a broadcast tower — arrows departing in many directions, few arriving. A bidirectional Star has both, and typically appears as the visual center of gravity for the entire graph.
The shape is significant beyond aesthetics: it tells you something about the module's role. A convergence point is something many things depend on. A broadcast tower is something that knows about many things. A bidirectional Star is both — and it's the one that warrants the most immediate attention.

utils.py as a Star. Green nodes are importers; blue nodes are dependencies. The hub's visual dominance is immediate — the layout algorithm has positioned it centrally because everything else needs to be near it.
What to Look for in Your PViz Bundle
PViz produces a structured analysis bundle (and visualizations) that you can inspect directly or pass to an LLM for deeper analysis. The Star pattern surfaces clearly in the bundle through a combination of signals:
Fan-in and fan-out counts: The bundle includes these metrics for every module. The Star candidates are the outliers — modules where these numbers are significantly higher than the median for your codebase. Because the bundle covers every module, you don't need to guess where to look; you can identify the top of the distribution directly.
The importer list: For any high fan-in module, the bundle lists exactly which modules depend on it. This tells you two things: whether the fan-in is concentrated in one part of the codebase (less dangerous) or spread across architectural layers (more dangerous), and which teams or domains would be affected by a change. A module imported by 20 modules all in the same domain is a different problem than a module imported by 20 modules spanning infrastructure, business logic, and API layers.
The dependency list: For any high fan-out module, the bundle lists what the module imports. This is how you diagnose what kind of Star you have — is it importing from many different domains (a coordinator or dumping ground), or from many modules in a single domain (possibly appropriate)? The dependency list is also the starting point for the categorization work that drives decomposition.
SCC membership: If a Star is also a member of a strongly connected component — a cycle in the dependency graph — the problem is compounded. A Star inside a cycle is both widely depended upon and entangled with its own dependencies. The bundle flags SCC membership explicitly, so this combination is easy to spot.
A few specific things to look for when reviewing the bundle:
The obvious Star: The right threshold varies by codebase size and language conventions, but as a rough illustration, a module with fan-in around 20 in a mid-sized codebase is worth examining; one approaching 40 or more tends to indicate a serious architectural concern. What matters most is relative outliers — if the median fan-in for your codebase is 3 and one module has fan-in 35, that module warrants attention regardless of the absolute number.
The growing Star: Fan-in trending upward over time is a warning sign even if the current absolute number seems manageable. The bundle gives you a snapshot; comparing bundles across commits or releases shows you the direction of travel. A module that gains 5 new importers every quarter will be a serious problem within a year.
The disguised Star: In Python and TypeScript codebases, barrel files and package __init__ modules frequently create Stars that are invisible to surface inspection but show up clearly in a resolved dependency graph. The mechanism is straightforward: when a consumer writes from utils import foo, the edge in the graph runs from utils to the importing module — not from the submodule where foo is actually defined. If the consumer had written from utils.formatters import foo instead, the edge would run from utils.formatters directly. Because most consumers use the top-level import path, the barrel accumulates fan-in for every symbol it re-exports, even though it contains no logic of its own. The result is a module that looks load-bearing in the graph because, from an import-path perspective, it is.
The cross-layer Star: A Star that spans architectural layers — business logic importing from it, infrastructure importing from it, and API layer importing from it — is more dangerous than one confined to a single layer. The importer list in the bundle makes cross-layer concentration visible: if the modules depending on a Star belong to fundamentally different parts of the system, that's a signal the Star has absorbed responsibilities it should never have owned.
What to Do About a Star
The goal isn't to eliminate all high fan-in modules — some things legitimately need to be imported widely. The goal is to ensure that high fan-in reflects deliberate design rather than accumulated convenience.
Step 1: Categorize the Responsibilities
Open the Star and list everything it does. Be specific. Not "handles users" but "validates email format", "hashes passwords", "fetches user by ID", "formats user display name", "checks subscription status". You're looking for responsibility clusters — groups of related things that could live together somewhere else.
This step is harder than it sounds. Stars tend to have fuzzy, overlapping responsibilities — that's part of how they got there. A function that fetches a user and also formats their display name for a specific context is doing two things, but it may not be obvious from the code that those things are separable. The goal at this stage is not a perfect decomposition but a first pass at what the natural groupings might be.
It helps to annotate each function or class with the domain concept it belongs to, then look at the distribution. A Star where every function belongs to one of three clear domains is straightforward to decompose. A Star where the domains are unclear or heavily interleaved is harder — and the interleaving itself is information about how tightly the module's concerns are coupled.
Step 2: Find the Natural Splits
Most Stars contain two to five coherent modules trying to get out. A UserManager might contain authentication logic, profile management, and notification preferences — three things that could each live independently. A utils.py might contain string manipulation, date formatting, and HTTP client configuration — three completely unrelated categories that ended up in the same file.
Finding the splits means looking for the seams where one cluster of functions has no real dependency on another. If the authentication functions never call the notification preference functions and vice versa, they don't need to live in the same module. The dependency graph within the Star is your guide: functions that call each other belong together; functions that share no internal dependencies are candidates for separation.
Step 3: Establish Ownership Before You Extract
Before moving any code, determine who owns each cluster. This is the step most technical treatments of decomposition skip, and it's the step that most often causes decomposition efforts to fail.
Stars are politically loaded modules. High fan-in means many teams have implicit dependencies on the module's behavior — not just its interface, but the specific way it behaves today. When you propose splitting a Star, you're proposing a change that affects multiple teams, and each of those teams will have opinions about the right way to do it. Some will have concerns you haven't anticipated. Some will push back not because the decomposition is wrong but because they're worried about the migration burden.
Getting alignment before you start is faster than getting it after a PR that touches 40 import sites. Identify which teams are affected, explain what you're proposing and why, and establish who will own each new module once extraction is complete. A decomposition that creates three new modules with no clear owner is worse than the original Star — at least the Star has an implicit owner, however overloaded they may be.
Step 4: Extract Incrementally
Don't try to decompose a Star in a single PR. Extract one responsibility at a time. Create the new module, move the code, update the imports in the original Star, and update the callers of the original Star that were only using that responsibility. Run tests after each extraction.
The order matters: start with the responsibility that has the fewest callers. Early extractions are learning exercises — you'll discover things about the module's coupling that will inform how you handle the harder extractions later. A function you thought was independent may turn out to depend on private state in the Star. A responsibility you thought was self-contained may be implicitly relied upon by a dozen callers who never imported it directly. Early, low-risk extractions surface these surprises in a context where they're easy to handle.
Resist the temptation to clean up everything you see during extraction. If the goal is to remove a specific responsibility from the Star, do exactly that and nothing else. Side-by-side refactoring during a structural decomposition is how PRs grow to 2,000 lines and lose reviewers. Keep each extraction small enough to review in a single sitting.

Before decomposition, all callers route through a single hub. After, each caller imports directly from the module that owns the responsibility it needs — no single module carries the full blast radius of the original.
Step 5: Enforce a Ceiling Going Forward
Once you've decomposed a Star, consider setting a soft limit on fan-in for that module type. If your codebase has a policy that utility modules shouldn't exceed fan-in 15, violations become visible in code review rather than invisible until the next dependency graph audit. This doesn't need to be enforced automatically — a convention that's understood by the team and checked during review is often sufficient to prevent a new Star from forming in the same location.
Re-running your PViz analysis after the decomposition gives you a before/after comparison. The original Star's fan-in should be significantly reduced, and the new modules should have fan-in distributions that reflect their actual scope. If a new module immediately shows high fan-in, that's a sign the split didn't follow the natural seam — the callers still have more in common than you thought.
When a Star Is Acceptable
Not every high fan-in module is a problem. Some things genuinely need to be imported widely:
- Core domain types: A
Usertype imported by 50 modules is not a Star problem — it's a domain model. High fan-in for a type definition reflects appropriate centralization of shared structure. - Configuration and constants: Modules that define shared configuration, feature flags, or system-wide constants legitimately have high fan-in by design.
- Logging and observability interfaces: Every module that emits logs imports the logging interface. That's correct behavior, not a structural problem.
- Deliberately shared interfaces and protocols: Abstract base classes, protocol definitions, and shared interfaces are supposed to be widely imported. That's their purpose.
The test is not "how many modules import this?" but "does this module do one coherent thing, and is it imported widely because that thing is genuinely shared?" If yes, high fan-in is appropriate. If the module does many things and is imported widely for different reasons by different consumers — some importing it for the date utilities, some for the HTTP helpers, some for the user validation — you have a Star.
The fan-in number is a signal, not a verdict. The verdict comes from reading the importer list and asking why each one depends on this module. If the answers are all the same, the fan-in is earned. If the answers are all different, you're looking at a Star.
How This Connects to the Rest of the Series
The Star is the simplest pattern in this series to identify, but it rarely exists in isolation. A codebase with one large Star often has several smaller ones orbiting it. A codebase with many Stars that have grown over time is on its way to a Mesh — the pattern we'll cover in Post 3, where the dependency structure becomes so dense that the graph loses meaningful shape entirely.
The Star is also closely related to the Creeping Utility (Post 7), which follows a similar formation pattern but focuses specifically on the lifecycle of helper modules — how they start as legitimate shared infrastructure and gradually become load-bearing walls that nobody wants to touch. If your Star is a utils.py or helpers.ts, that post will be particularly relevant.
And if you find a Star that sits at a layer boundary — infrastructure importing from it, business logic importing from it, API layer importing from it — that's a preview of the Upward Arrow pattern (Post 6), which covers what happens when architectural layers bleed into each other.
The vocabulary compounds as the series progresses. Once you can identify a Star, you'll start seeing the conditions that produce the more complex patterns, which makes those patterns easier to diagnose when you encounter them.
Conclusion
Stars are the most visible architectural problem in a dependency graph — and one of the most fixable. They're visible because the metrics make them impossible to ignore: fan-in and fan-out are straightforward to compute, and outliers are immediately obvious. They're fixable because the decomposition path is well-understood: categorize the responsibilities, find the natural splits, establish ownership, extract incrementally.
The danger of Stars isn't that they're hard to fix. It's that they're easy to ignore when you can't see them. Most teams know, informally, that certain modules are "complicated" or "risky to touch" — but without structural data, that knowledge lives in the team's oral history and disappears when people leave. A dependency analysis makes the pattern concrete, transferable, and actionable.
If your bundle shows a module with fan-in or fan-out that's significantly above your codebase median, that's where to start. Not because every outlier is a problem — but because the outliers are where the graph is telling you something worth hearing.
Analyze your dependency graph at pvizgenerator.com →
Published in the Reading the Graph Series
The Chain: Deep Dependency Paths and the Teams That Can't Ship Fast
How long linear dependency chains create invisible coupling between unrelated parts of your system
The Mesh: When Everything Depends on Everything
What to do when your dependency graph stops having structure
The Hourglass: Your Most Dangerous Module Is Probably One You've Never Thought About
High fan-in above, high fan-out below — and a single module holding it all together
The Island: Isolated Clusters and the Ownership Vacuum They Create
Why self-contained subsystems are more dangerous than they appear
The Upward Arrow: When Infrastructure Knows About Business Logic
Abstraction inversion and how to fix it
The Creeping Utility: How Helper Modules Become Load-Bearing Walls
The lifecycle of utils.py and how to intervene before it's too late
The Hidden Seam: Using Your Dependency Graph to Find Microservice Boundaries
Graph structure reveals where your monolith already wants to split
Try PViz on your own codebase
Get dependency graphs, coupling signals, and a compressed bundle ready for your LLM — for any GitHub repository, in minutes.