Reading the Graph: The Hourglass Pattern and the Structural Chokepoint

High fan-in above, high fan-out below — the hourglass module sits at a structural pinch point where everything converges. Learn how to find it, recognize it in code review, and decide whether to decompose it or protect it.

•15 min read
architecturedependency graphsrefactoringcode complexityreading the graph

Your team knows which modules are dangerous. The legacy payment processor that nobody wants to touch. The authentication service that breaks in mysterious ways. The database schema that took three months to migrate last time.

But there's a category of dangerous module that doesn't make it onto anyone's mental list — not because it's well-understood, but because it's invisible. It sits quietly in the middle of the dependency graph, not obviously complex, not historically troublesome. Until it becomes the center of an incident, most developers don't think about it at all.

That module is an Hourglass.

Understanding the hourglass pattern in software architecture is one of the more valuable things a dependency graph analysis can surface — because it reveals structural risk that code review, static analysis, and intuition all tend to miss.


What the Hourglass Pattern Looks Like

An Hourglass is a module with high fan-in and high fan-out — but unlike the Star, its fan-in and fan-out have a directional structure. The modules that depend on it sit in higher architectural layers (API handlers, services, orchestrators). The modules it depends on sit in lower layers (infrastructure, data access, external clients).

In a dependency graph, this creates a distinctive pinch-point shape: many nodes above it, many nodes below it, and a single module in the middle that everything must pass through. The top half of the graph connects to the bottom half through this one chokepoint.

It looks like an hourglass because that's exactly what it is — a narrow constraint through which a large volume of dependency flow must pass.

The pattern often appears in:

  • Facade modules that were designed to abstract a complex subsystem but accumulated more responsibility than intended
  • Service layer classes in MVC applications that grew to orchestrate every operation
  • Adapter or bridge modules that connect two major subsystems and gradually absorbed logic that belonged in both
  • Transaction managers or context managers that need to coordinate across multiple lower-level systems

The Hourglass Pattern — high fan-in from upper layers, single pinch point, high fan-out to lower layers


How Hourglasses Form

The Hourglass pattern is unusual among architectural problems in that it often starts as good design. Introducing a facade, a service layer, or an adapter is frequently the right call. The problem is what happens afterward.

The Accumulating Coordinator

A team introduces a service class to coordinate between multiple repositories. Good decision — it centralizes business logic and keeps the API layer thin. But over time, every new feature that touches those repositories routes through the service. It gains methods. Its fan-in grows. It starts importing additional infrastructure modules to support new requirements. Its fan-out grows. Nobody made a deliberate decision to create an Hourglass — it emerged from consistently routing new work through the same coordinator.

This is the most common formation path. The Hourglass isn't designed; it accretes. Every individual decision to route through the existing coordinator is locally sensible. The structural problem only becomes visible when you look at the dependency graph in aggregate.

The Necessary Chokepoint

Some Hourglasses form because the architecture genuinely requires a single coordination point. A payment system needs one place where all payment-related operations are authorized, logged, and validated. Making that a single module is intentional. The problem emerges when that module's boundaries aren't defended — when "payment-related" expands to include anything adjacent to payments, and the chokepoint accumulates scope beyond its original mandate.

The tell-tale sign of a Necessary Chokepoint that has gone wrong is a module whose name no longer describes what it does. If your PaymentService has methods for reconciling inventory, sending post-purchase emails, and updating loyalty tiers, the name is still PaymentService but the scope has long since left the building.

The Integration Layer

When two major subsystems need to be connected, teams often build a dedicated integration module. This module imports from both subsystems and is imported by anything that needs to cross the boundary. As more code needs to cross the boundary, more modules import the integration layer. As the integration becomes more complex, the integration layer imports more from both sides. An Hourglass emerges at the intersection.

Integration layer Hourglasses are particularly common at the seam between domain logic and infrastructure — the point where business rules meet database clients, message brokers, or external APIs. That seam is a natural place for coordination logic to collect.


A Concrete Example

Consider a mid-size e-commerce backend in Python. When a dependency graph analysis is run on the repository, order_service.py stands out: it has a fan-in of 18 and a fan-out of 14.

The fan-in of 18 comes from modules across the upper architectural layers — API route handlers, background job runners, a CLI management tool, an admin panel, and a webhook processor. Everything that touches an order imports order_service.

The fan-out of 14 comes from the modules below: the ORM models for orders, line items, and customers; the inventory client; the payment gateway wrapper; the email dispatch module; the fraud detection client; the shipping rate calculator; the tax engine; the loyalty points module; and the audit log writer.

On the surface, this looks reasonable. order_service.py is a service class — of course it imports a lot of things. But the structural problem becomes clear when you ask: what happens if the fraud detection client changes its interface?

The answer is that order_service.py has to change, and because order_service.py is imported by 18 upstream modules, every one of those modules is now at risk of a behavioral change they didn't ask for. The fraud detection client change has a blast radius that extends two hops in both directions through a single module.

When the dependency graph bundle for this repo is fed into an LLM for architectural review, this is exactly the kind of finding that surfaces: not a syntax error, not a failing test, but a structural pattern that makes the entire order domain fragile to changes in any one of fourteen dependencies.

The fix isn't necessarily to decompose order_service.py immediately. The first question is whether it's load-bearing — whether that fan-in of 18 reflects a genuinely necessary centralization of order logic or an accidental accumulation. The answer shapes everything that comes next.


What It Looks Like in Code Review

Engineers often sense the Hourglass before they can articulate it. The structural problem tends to manifest as a set of behavioral symptoms that show up repeatedly in pull requests:

Every feature PR touches the same file. When you look at the last twenty PRs that touched order logic, they all modified order_service.py. Not because it's wrong, but because it's the only place to put things. New developers learn this quickly: when in doubt, add it to the service.

Tests for unrelated features break together. A change to how orders handle tax-exempt customers breaks three tests that don't mention tax. The tests are actually testing different paths through order_service.py, and they share mocked dependencies at the service level. The coupling isn't visible in the code being changed — it's visible in the test scaffolding.

Mock setup for unit tests is excessive. Anyone writing a focused unit test for a single method on the service class has to mock fourteen dependencies. The test setup is longer than the test. Teams respond by writing fewer, broader tests — which means the module is effectively under-tested relative to its structural importance.

Code review comments frequently say "that doesn't belong here." Reviewers keep pushing back on additions to the file, but the PRs keep coming because there's nowhere else to put the logic. The file is a gravitational center. Without a deliberate decomposition strategy, the "doesn't belong here" comments don't change the trajectory.

The module's commit history is uniformly dense. Glancing at git log for the file shows a steady rhythm of changes from many different authors over many months. Unlike a genuinely stable module, the Hourglass changes constantly — it just changes for a lot of different reasons, which can make the churn look like active development rather than structural fragility.

If several of these patterns sound familiar for a specific file in your codebase, you're probably looking at an Hourglass.

5 Code Review Signals of a Hidden Hourglass


Why Hourglasses Are Dangerous

Single Point of Failure

An Hourglass is a structural single point of failure. If it's wrong — buggy, poorly performing, or incorrectly handling an edge case — every module above it is affected. Unlike a Star, whose failures tend to be localized to the concern it's managing, an Hourglass failure propagates to everything that flows through it. The higher in the architecture the downstream modules sit, the more user-visible that failure becomes.

Change Amplification

Changing an Hourglass has consequences in both directions. The modules above it may need to be updated to accommodate behavioral changes. The modules below it may need to accommodate interface changes. A modification that seems contained to the Hourglass itself can require coordinated changes across two architectural layers simultaneously — which means longer review cycles, more people involved, and more opportunities for regressions.

Hidden Blast Radius

The danger of the Hourglass is that its blast radius is larger than its fan-in suggests. A Star with fan-in 40 obviously affects 40 modules. An Hourglass with fan-in 18 appears to affect only 18 modules. But because those 18 modules are in higher architectural layers — closer to entry points — they represent a large proportion of the system's observable behavior. Changing an Hourglass effectively changes the behavior of the system's surface, even if the direct fan-in number looks manageable.

Underdocumented by Nature

Hourglasses are frequently underdocumented. They're structural artifacts rather than domain artifacts — they exist because of how the code is organized, not because they represent a meaningful business concept. Because they don't map to a clear business concept, they're harder to explain and harder to document. New developers find them confusing precisely because there's no intuitive narrative for why this module coordinates everything.

Test Complexity

Testing an Hourglass requires mocking everything below it and covering the needs of everything above it. With high fan-out below, the mock setup is extensive. With high fan-in above, the test scenarios multiply quickly. Teams often respond by testing Hourglasses superficially and relying on integration tests to catch problems — which means problems are caught late, in slow tests, with unclear attribution.


Identifying Hourglasses in Your Codebase

In a dependency graph analysis, look for modules that satisfy all three of these conditions:

  1. High fan-in: Many modules in higher architectural layers import it
  2. High fan-out: It imports many modules in lower architectural layers
  3. Positional centrality: It sits at or near a boundary between major architectural layers

The third condition is what distinguishes an Hourglass from a Star. A Star can appear anywhere in the graph. An Hourglass specifically sits at a layer crossing — it's the connection point between two halves of the architecture.

Practical ways to find them:

Look for modules where fan-in and fan-out are both above 10 and where the importers are in higher layers than the imported. This cross-directional structure is the defining characteristic.

Trace your critical paths. For your most important operations — user authentication, payment processing, order fulfillment — trace the dependency path from entry point to infrastructure. The module that appears on the largest number of critical paths, with the widest variety of modules above and below it, is your Hourglass candidate.

Run a structural bundle analysis on the repository and feed it to an LLM with a focused question: which modules have the highest combined fan-in and fan-out, and where do they sit relative to the API and infrastructure layers? The bundle gives the LLM the complete graph context it needs to answer accurately. A targeted question like this, grounded in real structural data, tends to surface Hourglass candidates quickly — much faster than reading through the codebase manually or relying on team intuition alone.

Ask your team which module they'd least like to change. Engineers have good intuitions about structural risk even when they can't articulate it in graph terms. The module that generates the most hesitation is often an Hourglass.


What to Do About an Hourglass

First: Determine Whether It's Load-Bearing

Some Hourglasses are intentional and correct. A well-designed facade that genuinely abstracts complexity is supposed to be a chokepoint — that's the value it provides. Before trying to fix an Hourglass, confirm that it's actually a problem.

Signs it's load-bearing and intentional:

  • It has a clear, coherent responsibility that's worth centralizing
  • Its scope has been stable over time
  • It's well-tested and well-documented
  • The team understands it and has opinions about what should and shouldn't be in it

Signs it's an accidental Hourglass:

  • Its responsibility is vague or has expanded significantly over time
  • Different team members describe its purpose differently
  • It's poorly tested relative to its importance
  • PRs that touch it tend to have unexpected consequences

If It's Accidental: Decompose the Responsibility

The decomposition strategy depends on what the Hourglass actually does.

If it's coordinating too many operations, split it by use case rather than by layer. Instead of one OrderService that handles order creation, refunds, fulfillment, and reconciliation, create OrderCreationService, OrderRefundService, and OrderFulfillmentService. Each inherits a portion of the original fan-in and a portion of the original fan-out. The total coupling in the system doesn't decrease immediately, but it becomes distributed — no single module is a chokepoint anymore.

The before state: 18 modules importing one coordinator, which imports 14 dependencies. The after state: 4–5 services, each imported by 3–5 modules, each importing 4–6 dependencies. Fan-in and fan-out are now proportional to actual responsibility.

Decomposing the Hourglass — one chokepoint becomes four small, independent paths

If it's a leaky facade, the module is abstracting a subsystem but allowing too much of that subsystem's internals to be visible to callers. Strengthen the abstraction — define a cleaner interface, hide the implementation details, and reduce what callers need to know about the lower layers. The fan-out may not decrease immediately, but the upstream modules stop depending on the specifics of what's below — they depend on the interface. This makes changes to the lower layer containable.

If it's an over-coupled integration layer, the integration module has absorbed logic that belongs in one or both of the systems it's connecting. Push that logic back. The integration layer should handle translation between systems, not implement business rules for either of them. When business rules live in the integration layer, changing either system requires changing the integration, and the blast radius of every change doubles.

If It's Load-Bearing: Protect It

If the Hourglass is correctly designed and genuinely necessary, the priority is protection rather than decomposition:

  • Establish and enforce strict ownership. One team is responsible for this module, reviews all PRs touching it, and actively defends its scope against accumulation. This is the most important protection measure — without it, the load-bearing Hourglass slowly becomes accidental.
  • Write comprehensive tests before any change is made. Not after, not during — before. The tests document current behavior and catch regressions. They also force the team to confront the full scope of the module's responsibilities before adding to them.
  • Document its contract explicitly. What callers can rely on. What the Hourglass guarantees about the systems below it. What belongs here versus what should be added elsewhere. This document is as important as the tests — it's the decision framework that prevents the module from accumulating scope by default.
  • Treat it as infrastructure. A load-bearing Hourglass should be treated with the same discipline as a database schema or a public API: changes require deliberation, deprecation has a process, and stability is a feature.

The Invisible Risk

What makes the Hourglass pattern particularly worth identifying is that it tends to fly under the radar until something goes wrong. It doesn't show up on anyone's "known risky modules" list. It doesn't generate a lot of day-to-day complaints — in fact, it often makes day-to-day development feel easier, because there's always an obvious place to put new logic. It sits quietly at the center of the architecture, routing everything through a single point, until the day it becomes the subject of an incident, a painful migration, or a three-week refactor that nobody planned for.

The dependency graph makes it visible before that day arrives. You don't need to experience the incident to know the risk exists — you need to see the structure. Fan-in, fan-out, and layer position together tell the story that the code itself doesn't advertise.

That's the core value of structural analysis: not finding problems you already suspect, but surfacing the ones you'd never think to look for.

Find structural pinch points in your codebase at pvizgenerator.com →


Published in the Reading the Graph Series

The Star: When One Module Knows Too Much
High fan-in, high fan-out, and the module your entire codebase orbits around

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 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.