DEV Community

TJ Sweet
TJ Sweet

Posted on

Building NornicDB With NornicDB: Code Graphs and Swarms

How we're using our own graph database to coordinate development, find duplicate implementations, profile performance, and keep concurrent agents from making a mess.

TL;DR

NornicDB's development environment now uses NornicDB itself as part of the engineering process.

The source code is indexed into a graph containing symbols, calls, references, documentation, and relationships. A custom harness retrieves that information before modifying code, checks proposed changes against existing implementations, delegates bounded work to different models, and requires verification before accepting the results.

The current setup includes:

  • NornicDB Graph RAG: Code-aware retrieval backed by the database we're building.
  • Duplicate detection: Reciprocal rank fusion (RRF), semantic similarity, and AST-based ownership analysis to catch reusable implementations.
  • Multiple models: Two local Qwen 9B workers, DeepSeek for larger bounded investigations and mechanical changes, and Codex for integration and final decisions.
  • Persistent execution: Asynchronous jobs, isolated Git worktrees, retained logs, and resumable task state.
  • Decision tracking: Engineering decisions and experiment outcomes extracted into the same graph, with source references and review status.
  • Release isolation: High-velocity development on develop, with individual changes cherry-picked onto dedicated release branches. Every change enters the release through a reviewable PR.
  • Independent testing: A separate 128-thread, 256 GiB TrueNAS system running up to 20 simultaneous database instances against different feature combinations.

My goal isn't to maximize the number of agents writing code. It's to increase useful engineering throughput without increasing duplication, regressions, or technical debt.


How We Got Here: The Main Branch Was Becoming a Nightmare

Originally, NornicDB development was simple: just me opening pull requests against main, reviewing them, merging them, and moving on.

That works when development is sequential, there's only one contributor, and changes don't regularly interact. It becomes a headache when development accelerates.

As we expanded NornicDB's compatibility coverage and ran more aggressive stress tests, we hit a wall of edge cases. Not obvious crashes, but subtle bugs in query results, transaction visibility, indexing behavior, and feature interactions that worked in isolation but failed under extreme concurrency.

Our maintainer, mm0nst3r, started exercising feature combinations our normal workloads weren't covering—which was exactly what we needed, but it meant the volume of issues, fixes, and concurrent PRs exploded.

We had agents working on new features while others investigated bugs. Fixes overlapped, depended on each other, or went stale before review. A PR might pass initial tests only to break after a related change landed.

We were leaning on GitHub PR state to mean too many things. An open PR didn't tell us whether implementation was underway, whether regression tests passed, or if it was waiting on another fix amid a mess of noise comments. Merging directly into main meant the rapid development cycle and the release integration cycle were the same process. It was a stressful, unmanageable nightmare.

The solution wasn't to stop concurrent development. It was to separate development from release acceptance.

Linear for Work State, GitHub for Code State

We integrated Linear with GitHub to track the phase of an issue separately from the state of its pull request.

  • Linear tracks the work: what needs to be implemented, what's under development, what's being tested, and what's ready for review.
  • GitHub tracks the code: branches, commits, PRs, review comments, checks, and merge state.

Separating these concerns gave us a clear boundary between fast-moving work and accepted release code.

Two Different Development Speeds

We now operate two distinct development cycles.

The first is high-velocity development on develop. This is where multi-model swarms investigate issues, implement features, run experiments, and fix bugs concurrently.

The second is the release cycle, which is intentionally slower.

The active release branch (e.g., release/v1.4.4) serves as the integration baseline. Changes are brought in individually with strict human review. We don't merge develop wholesale into the release branch. Instead, relevant commits are cherry-picked from development onto dedicated branches targeting the release.

Cherry-picking used to be a pain due to merge conflicts and cognitive overhead—something every pre-LLM team I worked on hated, even if it was cleaner. Now, LLMs handle that friction effortlessly.

A development branch often contains unrelated work or experimental code. Cherry-picking lets us pull over only what belongs in the release without dragging along the rest of the branch history.

The workflow looks roughly like this:

                    develop
                       |
          +------------+------------+
          |            |            |
       Feature A    Bug Fix B    Experiment C
          |            |            |
          v            v            v
      Development, testing, iteration
          |            |
          |            |
      Selected, verified commits
          |            |
          v            v
     Cherry-pick    Cherry-pick
          |            |
          v            v
     release-fix-A  release-fix-B
          |            |
          |  PRs targeting release/v1.4.4
          v            v
      Human review and verification
          |            |
          +------+-----+
                 |
                 v
          release/v1.4.4
                 |
            Full release
            verification
                 |
                 v
        Release branch PR
                 |
                 v
                main

Enter fullscreen mode Exit fullscreen mode

This setup lets develop keep moving while we stabilize a release, ensuring every change entering the release has a tight, reviewable scope. Nothing enters main except through the release branch PR process.

The Problem With Letting Multiple Agents Loose on a Codebase

NornicDB is a graph database written in Go supporting Cypher, transactional storage, graph and vector retrieval, and indexing. These subsystems interact in ways that aren't obvious from a single file.

A change to transaction publication can break index visibility. A storage optimization can pass an isolated benchmark while regressing a completely different workload.

Now add several coding agents working concurrently.

The primary issue isn't broken code; it's redundant code. An agent looking for a requirement searches a few nearby files, doesn't find an existing implementation, and writes a new one. It passes tests, but now you have two implementations of the exact same behavior. Multiply that across hundreds of PRs and you have a maintenance disaster.

Traditional text search doesn't catch two functions accomplishing the same thing through different abstractions. I wanted our harness to understand enough of the repository to spot those relationships proactively.

We already had a graph database. We might as well use it.

Indexing the Codebase as a Graph

The development harness maintains a code graph inside NornicDB using Graphify. It maps source symbols, calls, method ownership, references, and documentation.

Before modifying code, the harness can follow relationships across implementations. For instance, a node-deletion method at the storage-engine layer might be called through a transaction-specific wrapper. They are distinct symbols, but not independent implementations.

A duplicate-checking run might identify existing implementations across files like pkg/storage/badger_nodes.go (DeleteNode()) and pkg/storage/badger_transaction.go (the transaction-level delete path), while evaluating AST ownership to verify if they can actually be consolidated.

Semantic matching is only a candidate, not proof of duplication. Two methods with similar behavior might have different transaction ownership, locking, durability, or error-handling rules. Blindly merging them is an easy way to introduce a regression.

Retrieval Before Modification

Before applying a patch, the harness searches for existing code that might already implement the proposed behavior. It submits added patch text to NornicDB’s hybrid search, which combines lexical and embedding-based rankings using Reciprocal Rank Fusion:

RRF calculation

This combines rank positions without requiring the retrieval methods’ raw scores to be comparable. Embedding-based retrieval can surface related implementations even when names and wording differ. The results are advisory candidates for inspection—not proof that two implementations are equivalent.

Before committing, a deeper checker extracts eligible added or modified functions from an immutable snapshot of the Git index. It searches the stored code graph for each function, then evaluates retrieved candidates using code similarity and configured semantic-match rules, accounting for moved or removed implementations.

The staged snapshot prevents unrelated unstaged edits from changing what is checked. The graph’s indexed revision is tracked separately: a stale baseline remains a limitation, and duplicate detection supplements review rather than replacing it.

Using Multiple Models Without Pretending They're Interchangeable

Different models have distinct strengths, costs, latencies, and instruction-following profiles. We delegate work across specific tiers:

  • Local Qwen 9B workers (x2): Handle bounded mechanical work, quick code searches, validation, and engineering decision extraction.
  • DeepSeek Flash: Handles larger, well-specified investigations, refactoring proposals, and mechanical changes requiring wider context.
  • Codex: Responsible for integration, reviewing delegated results, resolving design decisions, and final acceptance.

Delegation doesn't transfer ownership. A worker can propose a change, inspect files, or run tests in an isolated worktree, but its output must still be reconciled against the codebase and task requirements. I don't care which model wrote a function as long as the implementation is correct, maintainable, and measurably better. The model is an implementation detail; the code isn't.

Cross-Model Review

A major benefit of this setup is that proposals aren't reviewed by the model that generated them.

For example, DeepSeek might investigate an allocation bottleneck and propose a schema index change. Codex can inspect that proposal, reject the heavy structural changes, and retain just the targeted micro-optimization - which is just one such instance.

Cross-model review consistently cuts out unnecessary complexity, ensuring we solve the problem with the smallest possible delta.

Top comments (0)