Building a Knowledge Graph of Your Codebase
Text search finds words. Questions about a system need relationships. How we model services, endpoints, events, and types as a typed graph, where the facts come from, and how it stays current.
By the Benlon team
Ask a search engine over your wiki "what breaks if I change the amount field on PaymentCreated?" and you will get every page that contains the word amount. Ask a senior engineer and you will get an answer: the ledger service consumes that event, the reconciliation job reads it nightly, and the partner webhook forwards it to customers.
The difference is not intelligence. The senior engineer holds a model of the system, made of services, what they expose, what they emit, who consumes what, and who owns it. That model is a graph, and it is the foundation Benlon is built on.
Why text search is not enough
Search indexes treat code and docs as bags of words. That works for "where is this string?" and breaks down for the questions engineers actually ask during onboarding, reviews, and incidents:
- Which services call this endpoint, and which of them are on the checkout path?
- Who consumes this event, and what happens if a field is removed?
- Which team owns the service that writes to this table?
- Has this component been involved in an incident before, and what was learned?
Every one of those is a question about relationships. To answer them reliably you need the relationships as data.
The shape of the graph
The graph is typed. Every node has a kind, and every edge has a kind that constrains what it can connect. A small slice of a payments system looks like this:
(team:payments-platform) -[:OWNS]-> (service:payments)
(service:payments) -[:EXPOSES]-> (endpoint:POST /v1/charges)
(endpoint:POST /v1/charges) -[:ACCEPTS]-> (type:CreateChargeRequest)
(service:payments) -[:PUBLISHES]-> (event:payment.created)
(service:ledger) -[:CONSUMES]-> (event:payment.created)
(job:nightly-recon) -[:READS]-> (datastore:payments-db)
(incident:2026-04-02) -[:INVOLVED]-> (service:ledger)The core node kinds are:
- Services and jobs: deployable units and scheduled work.
- Endpoints: HTTP, gRPC, and GraphQL operations, with their request and response types.
- Events: messages published to queues and streams, with their payload types.
- Types: the shapes that flow between everything else.
- Datastores: databases, tables, buckets, and caches.
- Owners, docs, and incidents: the human context attached to all of the above.
Edges such as EXPOSES, CALLS, PUBLISHES, CONSUMES, READS, WRITES, and OWNS carry the meaning. Because they are typed, the graph can be validated: an event cannot expose an endpoint, and a type cannot own a service.
Where the facts come from
Nothing in the graph is typed in by hand. Every fact is extracted from something that already exists in your repositories:
- Source code. Language parsers produce syntax trees, and framework-aware extractors recognize route declarations, handler registrations, message producers and consumers, and type definitions.
- Contracts. OpenAPI, protobuf, GraphQL, and AsyncAPI files are first-class inputs, because they are already the most precise description of an interface.
- Configuration and infrastructure. Deployment manifests and infrastructure code reveal which services exist, which datastores they use, and how they are wired together.
- Ownership.
CODEOWNERSfiles and service catalogs map code to teams. - History. Commits, pull requests, and linked incident write-ups attach context about why things are the way they are.
Every fact carries provenance: the repository, file, line, and commit it came from. Provenance is what lets generated docs link back to source, and what lets an answer cite its evidence instead of asking to be trusted.
Being honest about what is unknown
Static analysis cannot resolve everything. A service might build a URL from configuration, or publish to a topic name computed at runtime. When an edge cannot be resolved with confidence, it is stored as unresolved rather than guessed. Unresolved edges show up as gaps in the docs, which is more useful than a confident diagram that happens to be wrong.
Keeping it current
A graph that is rebuilt weekly drifts just like a wiki. So the graph is updated incrementally: on every merge, only the changed files are re-extracted, the affected part of the graph is recomputed, and the difference is emitted as a set of semantic changes:
+ endpoint POST /v1/refunds (service:payments)
~ type CreateChargeRequest.currency optional -> required
- event payment.legacy_settled (service:payments)
! consumers of payment.legacy_settled: service:reportingThose changes drive everything downstream: regenerated reference pages, flags on human-written sections that mention what changed, and warnings when something still depends on a removed event.
What the graph powers
- Reference documentation for every endpoint, event, and type, regenerated on each change.
- Architecture diagrams drawn from the graph instead of from memory, so they include the service added last week.
- Impact analysis that answers "what depends on this?" before a change ships.
- Grounded answers. When you ask Benlon a question, it retrieves the relevant part of the graph and the source it was extracted from, and answers with citations, not with whatever sounds plausible.
- Onboarding paths that walk a new engineer through a system in dependency order.
Design choices worth calling out
- Read-only by default. Building the graph needs read access to code and contracts, nothing more.
- Provenance everywhere. Every node and edge can be traced to the line it came from.
- Humans can annotate. Descriptions, caveats, and "why" notes attached to nodes survive regeneration.
- Unknown beats wrong. When the graph is unsure, it says so.
If you want to see what the graph looks like for your own system, book a product tour or read how to connect your first repository.
See living documentation on your own codebase
Connect a repository in minutes. Read-only by default.