Fan-in and Fan-out
How many modules depend on this one, and how many does it depend on. Both are signals about where change lands — and neither, on its own, is a verdict.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
A module has a lot of dependents, or a lot of dependencies. Which of those is a problem, and how would I tell?
A dependency report is circulated. Two modules top the list: Money, which almost everything imports, and CheckoutOrchestrator, which imports almost everything. A team lead wants both "fixed".
Cap them. Any module with more than N dependents gets split; any module with more than N dependencies gets refactored. It is objective, it is enforceable in CI, and it stops the argument.
Splitting Money because two hundred modules import it produces Money, Currency and Rounding, all of which those two hundred modules now import. Total edges went up and nothing changed (Over-Decomposition).
- Splitting
Moneybecause two hundred modules import it producesMoney,CurrencyandRounding, all of which those two hundred modules now import. Total edges went up and nothing changed (Over-Decomposition). - The cap on dependencies is satisfied by a facade:
CheckoutOrchestratorimports oneCheckoutFacade, which imports the twelve. The number is fixed, the coupling is identical, and there is now an extra layer (Facade). - Genuine problems are missed entirely. The module that couples eleven others through a mutable context object has a fan-out of one and is the worst thing in the codebase.
- Meanwhile the rule punishes the codebase's best asset: a widely used, stable, dependency-free value type is exactly what high fan-in should look like (Stable Dependencies).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- The report is generated from imports, so it sees static edges and misses everything that couples through shared state, events or configuration (Shared-State Coupling).
- Whatever is decided has to survive being turned into a build rule, because that is what will happen to it.
- Two of the flagged modules are on the critical path and have no meaningful test coverage, so any restructuring is high risk (Refactoring Without Tests).
- Any restructuring driven by these numbers must not change behaviour, and must not increase the total number of places a rule lives (Duplicate Knowledge).
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- A high fan-in module owns being stable. Its dependents did not consent to churn, so it takes on a compatibility obligation the moment it is widely adopted (API Stability).
- A high fan-out module owns composition and should own no rules — if it also decides things, it has acquired every reason to change that its dependencies have.
- Whoever reads the report owns the interpretation. A number without a reading is a suggestion to do something arbitrary.
- Fan-in defines a boundary's importance: the more dependents, the more the interface is a contract and the less freely the internals can move (Stable Boundaries).
- Fan-out defines a module's exposure: each dependency is a reason to change that it did not choose (Volatile Dependencies).
- The seam worth drawing is where a high-fan-in module has more than one reason to change — that is when its dependents start paying for churn that has nothing to do with them (Divergent Change).
Two modules, the same number, opposite verdicts
Both modules in the report have a large number of dependents. One is the healthiest thing in the codebase and the other is the most expensive, and the count does not distinguish them. The variable it is missing is how often the module changes.
Money has many dependents and no dependencies and changes about once a year. Formatting has the same dependents, imports the locale service, the currency table and the feature-flag client, and changes most weeks — so every one of those weeks ripples outward to everything that formats anything (Change Amplification).
- Fan-in alone cannot tell these apart. Fan-in times change frequency can, and that product is what people mean when they say a module is a bottleneck.
Formatting's problem is its fan-out, not its fan-in: it inherited three volatile dependencies and passes their instability to everyone (Volatile Dependencies).- The fix is not to split
Formattinginto three. It is to move the volatile parts out from underneath the stable interface so the widely used part stops changing (Stable Boundaries).
The four quadrants, and which one is the finding
Crossing the two numbers gives four positions, and only one of them is reliably a problem. That is a poor return for a metric, which is why fan-in and fan-out belong in a conversation rather than in a build rule.
The quadrant to look for is high fan-in with high fan-out: a module many things depend on that has itself imported many reasons to change. Everything it inherits, it broadcasts.
- Three of four cells are fine. A metric where most values are fine and the interesting case needs two numbers plus change frequency is not a threshold, it is a prompt.
- The top-right cell is where
utils,commonandcoremodules live, and it is the reason those names are a design smell rather than a naming preference (The Utility Dumping Ground). - Nothing in this table sees coupling through shared state, events or configuration, and those are the kinds that cost most (Kinds of Coupling).
| Low fan-out (depends on little) | High fan-out (depends on much) | |
|---|---|---|
| High fan-in (many depend on it) | The best shape in a codebase: a stable, widely used value type or domain rule. Money, UserId, the pricing rule. It is expensive to change *deliberately*, which is a fair price for not being changed accidentally (Stable Dependencies). | The finding. Every reason to change it inherits is broadcast to every dependent. Usually a "core" or "common" module that grew (The Common Module). Split by reason to change so the stable part stops moving. |
| Low fan-in (few depend on it) | A leaf: a feature module, an adapter, a job. Nothing here to look at, and most of a healthy codebase lives in this cell. | A composition root, a controller, a test fixture, an orchestrator. Correct by construction *if it holds no rules*. If it also decides things, it is a god object with a good excuse (God Object). |
What to actually do when the report lands
The report is not useless — it is a cheap way to generate a shortlist. The mistake is treating a position on the list as a diagnosis rather than as a reason to go and look at one module for ten minutes.
The question that turns a number into a finding is always the same: what changes, how often, and who pays? A module nobody has changed in a year is not a problem no matter how many dependents it has.
What is the module for, how often does it change, and who bears the cost of that change?
when Money, UserId, a pure domain rule.
cost Do nothing structural. Spend the attention on its tests and its interface stability instead, because its dependents cannot absorb churn (API Stability).
when Formatting, a shared core module, a widely imported config object.
cost The real finding. Split by reason to change and push the volatile part behind a boundary, accepting that every dependent gets a migration (Cohesion).
when A composition root, a controller, a pipeline definition.
cost Correct as it is. Verify it holds no rules — any conditional business logic in a wiring module is the thing to extract, not the count (Wiring and the Composition Root).
when A service that imports twelve collaborators and contains the pricing rules.
cost Extract the rules into a dependency-free module and leave the orchestration behind. Costs a real refactor; buys a testable core (Functional Core, Imperative Shell).
when The module every incident review mentions, with two imports.
cost The report cannot see this and it is often the most expensive module in the system. Go looking for shared state, event ordering and configuration coupling by hand (Shared-State Coupling).
How to build it
Most important first.
- Read fan-in together with stability. High fan-in on something that never changes is an asset; high fan-in on something that changes weekly is a change amplifier and the actual finding (Change Amplification).
- Read fan-out together with the module's job. A composition root, a controller or a wiring module is supposed to depend on many things; a domain rule that depends on twelve is doing more than one job (Wiring and the Composition Root).
- When high fan-in and instability coincide, split by reason to change rather than by size, so dependents that only need the stable half stop paying for the volatile half (Cohesion).
- When high fan-out is genuine, check the direction before the count. Twelve dependencies on stable owned types is fine; three on volatile external clients is worse (Dependency Direction).
- Use the numbers to generate questions, never to generate work items. "Why does this change so often given who depends on it" is the useful sentence (Design Review).
- If you must have a build rule, make it about direction and cycles rather than counts — those have unambiguous readings (Dependency Cycles).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- High fan-in, stable: the next change to it is expensive by design — it is a contract change touching many dependents — but changes to it are rare, so the expected cost is low. That is the trade a widely used type makes.
- High fan-in, unstable: every change ripples to all dependents, and the changes are frequent. This is the expensive quadrant and the one worth finding (Change Amplification).
- High fan-out, orchestration: the next change is usually a compile error and mechanical. Cheap per change, but the module is in the blast radius of everything, so it changes often.
- High fan-out, rule-holding: the next change is expensive to reason about, because a change in any of twelve dependencies can alter behaviour and no test covers the combination.
- What none of these numbers price: the coupling that does not appear as an import at all. A module with fan-in of two and a shared mutable structure can be the most expensive thing in the system (Shared-State Coupling).
- Interpreting these numbers well needs context that only someone familiar with the codebase has, which makes it exactly the kind of judgement that does not scale and gets automated badly.
- Acting on high fan-in by splitting improves the module and slightly worsens every dependent, and there are usually a lot of dependents (Module Granularity).
- The metrics are cheap and available, and cheap available numbers crowd out expensive accurate ones. Publishing them at all carries a risk that they become targets (Afferent and Efferent Coupling).
What can go wrong
- The counts get into CI as a threshold and are satisfied by indirection, which improves the metric and worsens the codebase (What to Automate Out of Review).
- A high fan-in module is split to reduce the number, and every dependent now imports two modules — the report improves and every consumer got slightly worse.
- High fan-out is treated as an aggregation problem and solved with a god facade, concentrating the dependencies rather than removing them (God Object).
- The mitigation fails on its own terms: interpreting the numbers case by case takes real senior attention, and a team without that attention will either ignore the report or automate it — and automating it is the failure above.
- Fan-in is dependency inward: the module's freedom to change is inversely proportional to it.
- Fan-out is dependency outward: the module's stability is bounded by the least stable thing it depends on.
- The two multiply where they meet. A module with many dependents that also depends on something volatile transmits that volatility to all of them, and that combination is the one worth acting on (Afferent and Efferent Coupling).
- "High fan-in is bad." High fan-in on a stable module is the sign of a well-factored codebase: everyone uses one
Moneyinstead of twelve. The problem is high fan-in plus frequent change (Stable Dependencies). - "High fan-out is bad." A composition root, a controller and a test fixture all have high fan-out and are correct. Fan-out is only a finding when the module also holds rules (Wiring and the Composition Root).
- "Reduce the number and the coupling reduces." A facade reduces the count and changes nothing about who depends on what. If the fix does not change what breaks when something changes, it fixed a number (Facade).
- "The dependency graph shows the coupling." It shows static imports. Shared state, events, configuration order and database tables written by two modules are all invisible to it, and those are the expensive kinds (Kinds of Coupling).
- god-object
- shotgun-surgery
- utility-dumping-ground
Testing it, and how it ages
- Test high-fan-in modules to a much higher standard. The number of dependents is a reasonable way to prioritise a limited testing budget, which is a use of the metric that does not require it to be a verdict (What a Unit Is).
- Give high-fan-in modules contract tests so a change that breaks a dependent fails in the module's own suite (Contract Tests).
- For high fan-out, test the composition with fakes for everything and test each dependency separately, or the suite becomes an integration test with twelve reasons to be flaky (Test Doubles, Precisely).
- Do not write a test that asserts a fan count. It measures imports, and imports are the easiest thing in a codebase to fake.
- Fan-in grows monotonically for anything useful. The moment to act is not when it crosses a number, but when the module first changes for a reason its dependents do not care about (Divergent Change).
- Fan-out grows as a module accretes responsibilities, so a rising fan-out on a rule-holding module is a genuine early signal of a god object (God Object).
- Both readings decay as a codebase adopts events and dynamic wiring, because the import graph stops describing the real dependencies — which is worth knowing before you build a dashboard on it.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThat dependents constrain your freedom to change and dependencies import reasons to change is structural, so it holds for modules, packages and services alike; only the cost of a rippling change scales with the grain.
- SIMULATEDThe dependent counts used in the examples below come from an Engineer Atlas model of a mid-size codebase, not from measuring a real repository. The shape transfers — a stable value type accumulates dependents while a rule-holding module accumulates dependencies — but no threshold in this lesson should be treated as calibrated, and any number you adopt from it would be arbitrary.
- CONTESTEDThe strongest opposing view is that these metrics, tracked over time rather than read as absolutes, are among the few early signals of structural decay that can be gathered without senior attention, and that refusing to act on them means acting on nothing until the damage is obvious. That is a real argument, and the honest response is not that the numbers are meaningless but that a *trend* on one module is evidence while a threshold across a codebase is not — and thresholds are what organisations actually implement.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — the same two numbers at service grain become request fan-out and blast radius, where a high-fan-in service is a single point of failure rather than a merge bottleneck.