What Semantic Graph is, and why everyone should be talking about it
Since February I have been building a tool at work called Semantic Graph. I am going to write a few posts about how it works, because the interesting decisions are in the details. But before the details, this is the post about what it is and why we bothered.
The question
If you work in a big codebase, you know the question. Before anyone touches anything, someone asks: what breaks if we change this?
Where I work there are a few hundred repositories. Some of them are old. They call each other through shared libraries, HTTP and Kafka, and some of the most important logic lives in database triggers. The honest answer to “what breaks” is usually: we think we know.
The way you find out today is manual. You grep for the name, open some files, follow a few calls, and ask the person who wrote it if they are still around. At some point you stop and hope you did not miss one.
Giving the question to an LLM does not fix this. It runs the same grep, reads the same files, and stops in the same place. It just does it faster, and charges you per token for every line of source it reads on the way.
The experiment
Early on, when the tool was a proof of concept indexing just two repositories, we ran a test. Two repositories does not sound like much. Between them they have close to ten thousand files, over two hundred thousand code entities, and about as many edges connecting them.
We asked the same assistant, Claude, the same question twice. Once with only the repositories on disk. Once with the tool available too. The question was about a SQL file that updates one of the central tables: what would be affected if we changed it?
| With the tool | Without | |
|---|---|---|
| Time | under 2 minutes | over 5 minutes |
| Tokens spent | around 60,000 | around 600,000 |
| Affected entry points found | over a hundred | 16 |
| Transitive callers | over 800, listed one by one | “41+ files”, an estimate |
| Same answer every run | yes | no |
In headline form: ten times fewer tokens, about 85% more of the impact surface found, and the same answer every time you ask.
Three things in that table matter more than the speed.
It was missing things. Without the tool, the assistant found 16 of the affected entry points. That is about 15% of them. The other 85% would have been found in QA, or in production. And this is not specific to REST endpoints. Any question that means following dependencies has the same gap: which triggers cascade from this table, which services sit in the call chain, which other repository consumes this interface.
It cost ten times as much. Almost all of those 600,000 tokens were raw Java the model read to find the few lines that mattered. Most of it was noise. With the tool, that work happens once, at indexing time. At query time the model gets structured data and nothing else.
It was not reproducible. Run the manual analysis twice and you get two different answers, because the model makes different choices about which files to open. The graph is resolved once and every query walks the same edges. Ask today or next month, same code, same answer.
That experiment is the reason the tool stopped being a proof of concept.
What it indexes
The short version: everything we could get a parser for.
Code. Java, Kotlin, C++, C#, Python, TypeScript and Scala. For each file it extracts classes, interfaces, methods, fields and annotations with their arguments. Then it resolves the calls: who calls what, across overloads and across the interface-to-implementation boundary. It also records inheritance, dependency injection, HTTP endpoints, and Kafka topics with their producers and consumers. Calls between repositories are resolved too, which is where most of the value is. Between the two repositories of the experiment alone there were over thirty thousand cross-repository call edges.
Databases. Tables, columns, constraints, indexes, views, sequences, functions, procedures and triggers, plus the migrations that created them. One of our schemas has more than two thousand tables, around eight hundred functions and five hundred triggers. It indexes in well under a minute. Once it is in the graph you can walk from a SQL file, through the triggers it fires, into the Java that reads the table. From there, up to every endpoint that depends on it. That path is invisible to grep.
Configuration. The Spring Cloud Config repositories, so “where is this property set, and which environments override it” has an answer.
Wikis. Confluence spaces, mostly runbooks and design notes. When something breaks, the procedure for it is usually written down somewhere. The problem was always finding it.
Tickets. Jira incidents, linked back to the areas of code they touched. The history of what broke before is context an engineer has after a few years on the team. Now the tool has it on day one.
The important part is that none of this sits in separate boxes. Every repository goes into the graph, and the edges cross repository boundaries, and product boundaries too. A call from a service into a shared library is one edge. A table, the trigger on it, the Java that reads it and the config that points at the database are all connected. So are the wiki page that explains the procedure and the ticket from the last time it broke. You can start anywhere and follow the relationships across the whole company’s code, not just inside the repository you happen to have open. That is the difference between a search index and a graph.
Today that is almost five hundred repositories across all our products, and where one product calls into another, that edge is there too. An average repository indexes in under a minute. The biggest one takes about eight minutes, down from an hour and a half in the first version.
What you ask it
The tool exposes an API, and the assistant reaches it through MCP. So you ask in plain language and the assistant picks the call. The questions people actually ask:
- What breaks if I change this method? You get the callers, the callers of the callers, and every entry point that reaches it, grouped by layer. On the most connected method in the codebase this takes under a second.
- What touches this table? The triggers, the functions, the code that reads and writes it, and the endpoints above them.
- Who injects this service?
- Find the code that does X, when you do not know what anything is called.
- What incidents have we had in this area, and is there a runbook for it?
What changed
The obvious gain is the one in the table: a tenth of the tokens, a fraction of the time, and an answer that is complete. The less obvious ones showed up after a few weeks.
- Impact analysis happens before the change, not in code review. Nobody discovers in review that their change touches eighteen endpoints they had never heard of.
- New people explore the codebase by asking, instead of spending weeks building the mental model by reading.
- During an incident you trace from the symptom back through the call chain in minutes.
- Reviewers check impact claims against the graph instead of trusting them.
- And the one I care most about: an agent that can ask “what breaks” before it edits is an agent you can leave alone for longer.
What it is made of
For the curious, the stack in one paragraph. Parsers built on tree-sitter. A Memgraph property graph for the structure, one per product, linked where products call each other. One Elasticsearch cluster for the vector index and the full-text search. A small embedding model, BGE-small, running locally on CPU. A FastAPI service in front, an MCP server on top of that, and Helm charts to deploy it all on Kubernetes. Well over a thousand tests, including golden tests that run the real parsers against real repositories. The only way to know the graph is right is to check it against code you know.
Each of those was a decision, and some of them I got wrong the first time. That is what the rest of the series is about. Why two databases and not one. The spike that failed and made everything faster. How you link a call to its method without a compiler. And how you shape an API for a caller that guesses.