Beyond Code Coverage: Dependency Diff Reviews for Catching Architectural Drift

Code coverage tells you whether your code works. Dependency analysis tells you whether your architecture still makes sense. Most teams only automate one.

•14 min read
code reviewci-cdarchitecturepull requests

Your CI pipeline probably checks:

  • Do tests pass? ✓
  • Is code coverage above 80%? ✓
  • Does the linter approve? ✓

But it probably doesn't check:

  • Did this PR introduce new circular dependencies? ❌
  • Did coupling increase in the payment module? ❌
  • Are we adding dependencies to a module we're trying to deprecate? ❌
  • Does this change violate our architectural layers? ❌

Code coverage tells you whether your code works. Dependency analysis tells you whether your architecture still makes sense over time. Both matter — but most teams only automate one of them.

The result is predictable: well-tested systems that slowly develop architectural drift — increasing coupling, circular dependencies, and hidden module entanglement that make the system harder to understand and evolve.


The Problem: Invisible Architecture Decay

Architecture rarely breaks all at once. It decays gradually, one pull request at a time.

// PR #847: "Quick fix for user validation"
// Added one import
import { PaymentValidator } from '@/payment/validators';

// Seems harmless, right?

Six months later:

// User module now depends on:
// - Payment module
// - Order module
// - Inventory module
// - Shipping module
// - Notification module
//
// Can't extract User to a microservice anymore
// Can't test User without mocking 5 modules
// Can't understand User without understanding the entire system

How did this happen? Each PR added "just one import." Each passed code review. Each had high test coverage. But nobody was looking at the dependency graph — the full map of how modules depend on each other.

This is the nature of architectural debt: it grows slowly, hides in plain sight, and is rarely tracked by CI metrics. Test coverage appears on dashboards. Circular dependency counts usually do not. Coupling drift rarely triggers alarms. Layer violations quietly accumulate.

Eventually someone attempts a refactor, a service extraction, or onboarding a new developer — and discovers the architecture has quietly turned into a distributed monolith in disguise.

A Hypothetical Worth Thinking Through

Imagine a SaaS team starting with a clean architecture: clear layers, well-defined module boundaries, and no circular dependencies. Six months later, after normal feature development, the same system may look very different.

Month 0: Clean Architecture

   Presentation Layer (8 modules)
          ↓
   Application Layer (12 modules)
          ↓
      Domain Layer (15 modules)
          ↓
  Infrastructure Layer (7 modules)

Dependencies flow in one direction. Module fan-out is low. New developers can orient themselves quickly.

Month 3: Warning Signs

You start seeing changes like this in PRs:

PR #1247: "Add email notifications to order flow"
  + import { OrderService } from '@/domain/order'
  → Infrastructure importing domain (layer violation)

PR #1289: "Improve user profile validation"
  + import { PaymentValidator } from '@/payment'
  → Cross-domain coupling

PR #1312: "Quick fix for race condition"
  + import { UserService } from '@/user'
  → Circular dependency created (user ↔ payment)

None of these PRs look alarming in isolation. The author of PR #1312 probably didn't know that payment already imported user — that knowledge lives in the global dependency graph, not in the code diff.

Month 6: Architectural Drift

By now, circular dependencies have multiplied, layer violations have accumulated, module fan-out has increased, and tests pull in deeper dependency chains. PR reviews slow down because reviewers can no longer easily predict the impact of a change.

The team didn't do anything wrong. They shipped features. But without dependency analysis in CI, they gradually dismantled the structure that originally made the system understandable. The time to fix this wasn't month 6. It was PR #1247.

What Architectural Drift Actually Breaks

Architectural drift doesn't just make diagrams uglier. It changes how a codebase behaves operationally. Over time, uncontrolled dependency growth affects several practical aspects of development.

Test Isolation Degrades

Modules that once had clear boundaries begin pulling in deeper dependency chains. Unit tests gradually become integration tests because the system can no longer be easily decomposed.

Refactoring Risk Increases

When modules depend on many others, even small refactors require touching multiple parts of the system. Changes that once affected one component now ripple across multiple subsystems.

Build and CI Times Increase

Large dependency chains mean more modules must load during builds and test runs. This often manifests as gradually slower CI pipelines — one of the earliest and most overlooked symptoms of architectural drift.

Feature Velocity Slows

When developers cannot easily reason about dependency boundaries, they spend more time verifying side effects. PR reviews take longer because reviewers cannot confidently predict what a change might impact.

Onboarding Becomes Harder

New developers rely heavily on architectural boundaries to navigate unfamiliar codebases. When boundaries erode, understanding the system requires understanding nearly everything. Ramp-up time grows quietly alongside the coupling count.

Architectural drift doesn't usually trigger outages. Instead, it quietly reduces the rate at which the team can safely change the system.

Why Most Teams Don't Catch This

If dependency drift is such a common problem, why isn't everyone reviewing dependency graphs in pull requests?

Historically, two factors made this difficult. First, dependency graphs were often too slow or too cumbersome to generate as part of routine pull request review. Second, architecture was treated as a periodic concern — something you reviewed quarterly or when problems became obvious.

Neither assumption holds today.

Modern static analysis tools can generate full dependency graphs in seconds, which changes the economics completely. Instead of occasional architecture reviews, teams can monitor architectural change continuously — the same way they monitor test coverage. The tooling gap is largely closed. What remains is a process gap.

The Solution: Dependency Diff Analysis

Imagine your CI pipeline producing something like this on every pull request:

Dependency Analysis for PR #1847
Status: ⚠️  Architectural changes detected

Module Impact:
+ user/UserService.ts → payment/PaymentValidator.ts
  ⚠️  New cross-domain dependency

Metrics Changes:
  Coupling score (user module): 23 → 31 (+35%)
  Circular dependencies: 3 → 4 (+1)
  Layer violations: 8 → 9 (+1)

Recommendations:
  🔴 Block merge: New circular dependency created
  🟡 Needs review: Coupling increase >20%
  🟢 Approved: Test coverage maintained

This is dependency diff analysis. Instead of inspecting architecture occasionally, you analyze how each pull request changes the dependency graph. Just like test coverage alerts you when coverage drops, dependency diff alerts you when:

  • a circular dependency appears
  • module coupling increases beyond a threshold
  • architectural layer boundaries are violated
  • new cross-module dependencies appear

The core insight is simple: architectural issues are inexpensive to fix when the change is small, and extremely expensive to fix months later.

A Simple Example

Before the PR:

User → Order → Payment

After the PR:

User → Order → Payment
 ↑               ↓
 └───────────────┘

A circular dependency has appeared. Without a dependency graph comparison, this change may go unnoticed in review. With dependency diff analysis, the cycle is immediately visible.

Implementation: CI Pipeline Integration

The general pattern involves three moving parts: a dependency analysis service that inspects the repository and produces a structured artifact, CI jobs that generate and store those artifacts as snapshots, and comparison logic that diffs the baseline against each pull request.

The workflow looks like this:

  1. On each merge to main, submit the repository for dependency analysis and store the resulting JSON artifact as a CI baseline
  2. On each PR, submit the PR branch for analysis and retrieve a fresh artifact
  3. Compare the two artifacts to identify architectural changes
  4. Post the diff as a PR comment and optionally fail the build on critical issues

The key idea is to treat the dependency graph as a versioned artifact, similar to coverage reports or build outputs. Once the graph exists as structured data, comparing architecture across commits becomes practical to implement.

What Dependency Diff Reviews Show in Pull Requests

When a PR introduces a blocking issue, the automated comment surfaces it immediately:

Dependency Analysis Report

⚠️ Blocking Issues

New Circular Dependency Detected:
  user/UserService → payment/PaymentValidator → user/UserValidator → user/UserService

  Impact: 3 modules now in a dependency cycle
  Recommendation: Extract shared validation interface to break the cycle

Instead of discovering these problems months later, reviewers see them before the code merges — when a fix is still a small change rather than a multi-sprint refactor.

Real-World Examples

Example 1: Catching a Circular Dependency

PR #1523: "Add order validation to user service"

+ import { OrderValidator } from '@/order/validators';

class UserService {
  validateUser(user) {
+   OrderValidator.checkPendingOrders(user);
  }
}

The dependency diff might report:

🔴 BLOCKING: Circular dependency created

Cycle: user → order → payment → user

Explanation:
- user imports order (this PR)
- order already imports payment
- payment already imports user

Suggested fix:
Extract shared validation logic to a neutral location:
  shared/validation/OrderRules.ts

The author of this PR almost certainly didn't know that payment already imported user. The cycle spans three modules and is completely invisible in a standard diff review. Dependency analysis surfaces it immediately and points toward a fix — the kind of feedback that turns a potential month-6 problem into a one-afternoon conversation.

Example 2: Layer Violation

# In infrastructure/email/OrderNotifier.ts
+ import { Order } from '@/domain/order/Order';
+ import { OrderStatus } from '@/domain/order/OrderStatus';

The dependency diff might report:

🔴 BLOCKING: Layer violation

infrastructure/email/OrderNotifier → domain/order/Order

Rule: Infrastructure should not import domain entities directly.

Suggested fix:
Use a domain event or DTO instead:

// domain/order/events.ts
export interface OrderShippedEvent {
  orderId: string;
  customerEmail: string;
}

// infrastructure/email/OrderNotifier.ts
notify(event: OrderShippedEvent) {
  this.sendEmail(event.customerEmail);
}

This pattern feels entirely reasonable in isolation — the notifier needs to know about orders, so it imports them. The layer rule makes the architectural implication explicit: infrastructure reaching into domain creates coupling that prevents the domain from being tested or reused independently. Surfacing the violation at the PR stage gives the author a concrete fix and a clear reason for it, rather than letting the pattern silently accumulate until it becomes load-bearing.

Example 3: Acceptable Coupling Increase

Not every dependency change is a problem. This is what a warning-level (non-blocking) report looks like:

PR #1712: "Add user preferences to profile"

+ import { ThemeService } from '@/theme/ThemeService';
+ import { LocaleService } from '@/i18n/LocaleService';
+ import { NotificationPreferences } from '@/notifications/preferences';
🟡 REVIEW REQUIRED: Coupling increase

user/UserProfileService:
  Before: 8 dependencies → After: 11 dependencies (+37%)

New dependencies: ThemeService, LocaleService, NotificationPreferences

Question: Is this coupling appropriate for a user profile service?

The right answer here is probably "yes" — user preferences legitimately touch themes, locale, and notifications. The team reviews it, agrees the coupling is appropriate, documents the rationale, and approves. That's the process working exactly as intended: not blocking reasonable work, but ensuring the decision was made consciously rather than absorbed without anyone noticing. A year from now, when someone asks why UserProfileService has those dependencies, there's an answer in the PR history.

Avoiding Noisy Architecture Checks

One common concern with architectural checks is false positives. If every PR produces warnings, developers will quickly learn to ignore them. Effective dependency diff systems usually address this in three ways.

Gradual rule enforcement. Start by reporting issues without blocking merges. Allow the team to learn what the signals mean before enforcing rules.

Threshold-based alerts. Not every coupling increase is problematic. Many teams only trigger warnings when coupling increases beyond a defined percentage threshold, filtering out routine growth from genuinely concerning drift.

Explicit architectural rules. Layer contracts and module boundaries should reflect the system's intended architecture. Rules that match the actual architecture produce useful feedback. Rules that don't produce noise. Like any automated review tool, dependency diff works best when tuned to the realities of the codebase.

Advanced Patterns

Dependency Budgets

Teams can set a hard dependency ceiling on important modules — particularly ones undergoing active cleanup or that represent critical isolation boundaries:

modules:
  payment:
    max-dependencies: 8
    current: 8
    budget-remaining: 0  # No new dependencies without architecture approval

  order:
    max-dependencies: 15
    current: 9
    budget-remaining: 6

When a PR would exceed the budget, reviewers are forced to consciously decide whether the new dependency is justified rather than absorbing it silently. This is especially useful mid-cleanup: lock down modules that are finally stable while continuing to allow growth in areas that still have headroom.

Co-Change Analysis

Static dependency graphs don't capture everything. Sometimes modules are tightly coupled despite having no direct import relationship — they simply tend to change together.

If two modules appear together in a large percentage of pull requests over a rolling window, that's a meaningful signal. It suggests hidden coupling: shared mutable state, implicit contracts, or a responsibility that doesn't yet have a formal home in the codebase. Analyzing commit history alongside dependency graphs can surface these patterns. When a PR touches both halves of a high-frequency co-change pair, that's a natural prompt to ask whether the architecture actually reflects the dependency that clearly exists in practice.

Practical Rollout Strategy

A phased rollout helps teams adopt architectural checks without generating friction or resentment.

Week 1 — Setup. Generate a baseline dependency graph on main, configure the CI analysis job, and define an initial set of architecture rules. The goal this week is purely mechanical: get the pipeline producing artifacts so you have something to work with.

Week 2 — Visibility Mode. Enable dependency diff comments on PRs but don't block merges yet. This is the most important phase. Developers need to see what the checks flag in the context of real work before those checks can fail their builds — otherwise enforcement feels arbitrary. Watch for misconfigured rules or false positives and note them for calibration.

Week 3 — Calibration. Review several weeks of flagged PRs to understand the codebase's normal coupling patterns. Adjust thresholds, refine layer rules, and eliminate false positives. Finalize severity levels with input from the team. The output of this week should be a ruleset the team broadly agrees reflects their actual architectural intent.

Week 4 — Enforcement. Block circular dependencies and critical layer violations. Keep coupling increases as warnings — review-required, not blocked. Document the process so new contributors understand what the checks are for and how to respond to them.

Teams that start with visibility tend to arrive at enforcement with buy-in rather than resentment. The difference is that developers understand the rules before the rules can stop their work.

Architecture as a First-Class Review Concern

Code coverage tells you if your code is tested. Dependency analysis tells you if your architecture is staying healthy. Most teams automate one of these checks but not the other.

The best time to catch an architectural problem is in the pull request that introduces it — when the change is still small and the fix is usually straightforward. Not months later in a painful refactor. Not during an architecture review meeting spent staring at a tangled diagram.

Dependency diff makes that possible. It turns architecture from something you inspect occasionally into something you maintain continuously — the same way test coverage turned testing from a periodic audit into a line in every CI run.

Your dependency graph should improve over time, not degrade. Making that progress visible is the first step toward making it automatic.


PViz analyzes software repositories and generates structured dependency graph bundles covering module relationships, coupling metrics, cycle detection, and architectural patterns across Python, TypeScript, JavaScript, Go, and Java. Those artifacts are designed for both human review and LLM-assisted interpretation, which makes them well suited to the baseline-and-diff workflow described here.

Next in this series: Onboarding Metrics — measuring codebase complexity to improve developer ramp-up time.

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.