Codapult
DocsBlogPricingPluginsDemo
Get Codapult
Logo

Build your SaaS with AI. Keep the architecture under control.

Get Codapult

Product

  • Pricing
  • Architecture
  • Modules
  • AI platform
  • Plugins

Developers

  • MCP
  • CLI
  • Documentation

Resources

  • Blog
  • FAQ

Connect

  • Contact
  • GitHub

Compare

  • SaaS template comparison
  • Codapult vs Supastarter
  • Codapult vs Makerkit
  • Codapult vs ShipFast
  • Codapult vs SaaSBold
  • Codapult vs Gravity
  • Codapult vs Nextbase
  • Codapult vs BuilderKit

Featured on

Codapult on LaunchNestCodapult on LaunchNestbetterlaunch.cobetterlaunch.coFeatured on LaunchBuffFeatured on LaunchBuffCodapult on PeerPushCodapult on PeerPushFeatured on LaunchItFeatured on LaunchIt
© 2026 Codapult·All rights reserved·Privacy Policy·Terms of Service
Full source code · One-time purchase · Self-host anywhere
Back to blog
September 25, 2026·11 min read·Codapult Team

A Dependency Graph Is Not Architecture

Why a dependency graph only shows current code relationships, and why AI coding agents need explicit architectural policy to avoid technical debt.

ai-agentsarchitecturemcpdependency-graphtypescript
Dependency graph of Client, Service, Repository and Database contrasted with a separate policy and intent layer

A graph can show how your code is connected. It can't tell you which of those connections your project actually wants to keep.

A dependency graph is one of the first things I wanted when I started thinking about tooling for AI coding agents.

It makes sense.

An agent needs to understand a codebase, and a graph is a much better representation of relationships than a pile of files.

You can see that a route depends on a service. That service depends on a repository. A repository depends on a database client. Another module imports the service. A shared utility is suddenly used by half the application.

That information is useful.

But after working with this problem for a while, I started thinking that there is a trap here.

A dependency graph is not architecture.

It is a description of relationships. Architecture also contains intent.

The graph tells you what exists

Suppose I have this:

Client
  ↓
Server Action
  ↓
Service
  ↓
Repository
  ↓
Database

A graph can represent that pretty well.

A dependency graph showing Client, Service, Repository and Database contrasted with architectural intent

It can tell me that the service imports the repository. It can tell me that the repository imports the database client. It can tell me which files are connected to a changed module. It can show cycles or unusually large dependency hotspots.

All of that is valuable.

Now imagine another part of the application has this:

Client
  ↓
API Route
  ↓
Database

The graph can show that too.

What it cannot tell me by itself is whether the second path is wrong.

  • Maybe it is.
  • Maybe the project deliberately allows direct database access for a reporting endpoint.
  • Maybe the second path is old code that nobody has touched in three years.
  • Maybe it was introduced last week and is exactly the pattern the team now wants to standardize on.

The graph contains all four possibilities. It has no way to choose between them.

Architecture has a "why"

This is where the difference matters.

Architecture isn't only a collection of edges between components. It also contains decisions like:

  • Payment provider access goes through an adapter.
  • Only server-side code can access persistence.
  • Module A must not depend on Module B.
  • This package is intentionally isolated because we may remove it later.
  • This service owns authorization for this operation.

Those are not just observations about the current repository. They are decisions. And some of them can exist even when the repository doesn't currently satisfy them.

That's an important distinction.

The code might currently contain:

Checkout → Stripe

while the intended architecture is:

Checkout → Payment Adapter → Stripe

If all you have is the current graph, the direct dependency is simply a fact. You don't know that it is a violation until someone tells you what is supposed to be true.

This is why "architecture inference" is tricky

I've seen a few different approaches to this problem.

One is to infer architecture from directory names. If there is a services directory, call it the service layer. If there is a repositories directory, assume everything under it is persistence.

That works until it doesn't.

Another project might use:

features/
domain/
infrastructure/

Another might put all of its business logic inside feature folders. Another might have very little layering at all. Another might have started with one architecture and gradually evolved into something else.

None of those structures is automatically correct.

So I don't really want a tool to look at a repository and say: "This is good architecture."

That seems like the wrong question. I'd rather ask:

  1. "What is this project actually doing?"
  2. "Which parts of that behavior has the team decided to preserve?"

Those are much more concrete questions.

A graph also captures accidental history

There is another problem. Real repositories contain history. Not all history is intentional.

A developer copies an implementation. Another developer adds a shortcut. Someone moves a file but keeps the old import. A feature gets rushed into production. Six months later there are three different ways to access the same thing.

The graph faithfully records all of them.

In that sense, a graph can actually make the ambiguity more visible without resolving it. You might have:

Feature A → Service A → Repository
Feature B → Repository
Feature C → Service C → Repository
Feature D → Service A

What should a coding agent learn from this?

It could conclude that both direct repository access and service access are valid patterns. It could pick the shortest path. It could copy the most common one. It could look at recent commits.

None of those decisions necessarily tells you what the project intended.

And if an agent starts using an accidental pattern as an example for new work, the inconsistency becomes self-reinforcing. That's a much bigger problem than just having an ugly graph.

The missing layer is policy

This is the part I keep coming back to. A useful project model has at least two different things:

Diagram showing repository facts, explicit policy and verification as three separate layers
  • Facts: What the repository actually contains.
  • Policy: Which of those facts describe behavior the project wants to preserve.

The graph is very good at the first one. It doesn't magically give you the second.

For example:

Facts:
Checkout imports PaymentAdapter
PaymentAdapter imports Stripe
RefundService imports Stripe

And then:

Policy:
All payment-provider access must go through PaymentAdapter

Now the system can check something meaningful. A direct Stripe import isn't merely "an unusual edge." It is a concrete violation of a known project decision.

That's a much stronger signal for an AI agent too.

This also makes exceptions easier to handle

Architectural rules almost always have exceptions.

Suppose the rule is: UI modules must not import persistence modules.

Then someone builds an internal admin export screen. The simplest implementation might legitimately need a different path.

Without an explicit policy model, you tend to end up with one of two bad outcomes:

  1. Either the tool becomes so strict that people stop using it.
  2. Or everyone starts ignoring the warnings.

An explicit policy gives you somewhere to put the exception. The project can say:

  • This boundary normally applies here.
  • This particular module is allowed to cross it.
  • Or: This is temporary and needs to be removed later.

That is much closer to how real architecture works.

Baselines matter for the same reason

The same thing happens with existing violations.

Imagine a large application already has twenty direct database imports from places that ideally shouldn't have them. The graph can find twenty edges. A naive architecture checker might immediately fail the project.

That's not especially useful. I don't need a tool telling me that yesterday's codebase was imperfect. I need it to help me stop making the situation worse.

So a more useful question is:

  • Which violations already existed?
  • Which ones were approved or baselined?
  • Which new violations did this change introduce?

That makes the graph part of a useful workflow instead of turning it into a giant list of problems.

The interesting part starts when an agent gets involved

This becomes more important when the repository is being modified by an AI coding agent.

An agent can query a graph and ask:

  • What depends on this module?
  • What imports this package?
  • What routes reach this service?
  • What will be affected if I change this file?

That's great. But then it needs another question: Am I allowed to make this dependency?

The first question can be answered from repository facts. The second requires policy.

If the system returns: "This import exists," that's one kind of information.

If it returns: "This import exists. This dependency crosses an approved boundary," that's much more useful. It gives the model something it can act on.

This is also where history gets interesting

Git history is useful, but I don't think history should be treated as policy either.

Suppose the agent sees that a particular dependency was added in a large refactor eighteen months ago. That tells you something. Maybe there was a reason. But it doesn't necessarily tell you whether the dependency is still intentional.

The same applies to code frequency. A pattern used 200 times is not automatically a good pattern. A pattern used once is not automatically wrong.

History can provide evidence.

Diagram separating current code structure, repository history and intended architectural policy

It shouldn't silently become a rule.

Why I built Guard around this distinction

This is one of the ideas that shaped Codapult Guard.

I didn't want to build another system that looks at a repository graph and declares what the architecture should be. Guard starts with the repository that already exists. It discovers facts about the project: files, imports, AST relationships, routes, capabilities, contracts, and other relationships derived from the code.

Then the project can decide which of those things should become policy.

After that, the useful part is verification. For example:

Client module → Database import

can be reported because the project explicitly decided that this dependency isn't allowed.

Guard doesn't treat every possible architectural preference as a universal rule. The project decides, the tooling checks, and existing violations can be baselined so future changes are checked cleanly.

The core checks are deterministic and local. There is no need to ask another model whether one module imports another.

A graph is still extremely useful

None of this means graphs are overrated. Quite the opposite.

A graph is excellent for:

  • What exists?
  • What depends on what?
  • What changed?
  • What might be affected?
  • Where are the cycles and hotspots?

Policy can answer:

  • What is allowed?
  • What is forbidden?
  • Which boundaries matter?
  • Which exceptions are intentional?

Verification can answer:

  • Did the change preserve those decisions?

Putting them together is much more powerful than pretending one representation can answer all of them.

The agent doesn't need a bigger graph

My first instinct was always: give the agent more information. More files, more history, more dependencies, more documentation, more graph data.

But eventually you hit diminishing returns. The agent doesn't necessarily need the whole graph. It needs the facts that are relevant to the decision it is making, and it needs to know which facts are constraints rather than observations.

For a change to a payment module, I probably don't need a thousand unrelated edges. I need:

  • This module is affected.
  • These modules depend on it.
  • These contracts apply.
  • This dependency is forbidden.
  • This exception is approved.
  • These tests and checks are relevant.

That's much closer to useful project context.

Maybe architecture is better thought of as a decision record

The shape is the visible part. The decisions are the interesting part.

  • Why is this dependency allowed?
  • Why is that one forbidden?
  • Why does this adapter exist?
  • Why is this module isolated?

A graph can preserve the shape. Policy can preserve some of the decisions. History can provide evidence about how we got there.

Combining them gives an agent a model of the system that separates:

what is true
  ↓
what is intended
  ↓
what changed

That separation is probably more important than making the graph itself smarter.

That's the part I'm still exploring

I don't think the answer is one giant "AI architecture brain" that understands everything in a repository. I'd rather have smaller pieces that do one thing reliably:

  • Repository analysis establishes facts.
  • Policy captures decisions.
  • Static checks verify deterministic constraints.
  • Tests verify behavior.
  • An agent reasons about requirements and implementation.
  • A human decides when the architecture itself should change.

If an AI can change a hundred files in one session, the environment must tell it which parts of that context are observations, which parts are constraints, and which decisions are still open.

A dependency graph is a very good start. It just isn't the architecture.


Turn architectural intent into explicit rules for your codebase and AI agents.
Codapult isolates domain logic from infrastructure using strict adapter boundaries (Auth, Payments, Database) and deterministic policy checks. Rather than relying on implicit conventions, it exposes explicit architecture contracts and MCP resources so both developers and AI agents know what connections are allowed before making a change. Explore the policy boundaries in the Codapult Architecture Docs.