← Building Memory for AI Coding Agents
03 / 06 Technical field note

The Case for One Postgres

Why a coding-agent memory system can avoid synchronized graph and vector stacks by keeping graph, vector, relational, and audit data inside one transactional Postgres database.

PostgreSQLpgvectorAI AgentsArchitecture

Most memory systems for AI agents ship as a stack. Mem0 wants Qdrant plus Neo4j. Zep wants Neo4j plus a vector store. Cognee wants three or more services. The reasoning is intuitive: graphs are a graph problem, so use a graph database; embeddings are a vector problem, so use a vector database; put them side by side and wire them together.

Borg runs on one Postgres. No Qdrant, no Neo4j, no sync daemons. This was a deliberate choice, and it’s worth explaining what it buys and what it costs.

What the multi-database stack actually costs

The moment you have two databases holding related data, you have a synchronization problem. An entity in the graph references an embedding in the vector store. When one changes, the other has to follow. That coupling is now your responsibility, and it fails in the quiet ways distributed systems always fail: a write succeeds in one store and not the other, a reindex lags, a deletion cascades in one place but not its mirror.

The principle Borg keeps coming back to is blunt: adding a graph DB means syncing two systems, and sync means drift. Every new store is a new consistency boundary and a new class of failure.

For an engineering leader, this is operational surface. Two systems to provision, monitor, back up, secure, and pay for, plus the glue between them, which is usually the least-tested code in the stack. For an engineer, it’s the class of bug that only shows up under load and never reproduces locally.

Postgres already does the hard parts

The multi-database instinct assumes Postgres can’t do graphs or vectors well. That assumption is out of date.

Graph traversal is a recursive CTE. Borg ships a single function, borg_traverse(), that walks one or two hops from an entity, stays cycle-safe by tracking the path, and scopes every walk to a single namespace. Vector similarity is pgvector, doing approximate nearest-neighbor search in the same query planner as everything else, on 1,536-dimension embeddings from text-embedding-3-small. Full-text is a GIN index. Audit is pgAudit. The entire system is fifteen tables and that one function, on Postgres 14 or newer.

Because graph, vector, and relational data live in one engine, a single query can join across all of them. You can find vector-similar candidates, walk their graph neighborhood, and filter on typed facts in one planned, transactional operation. In a two-database design that same logic becomes application code shuttling data between services, with no transaction spanning the whole thing.

A schema built around consistency

The fifteen tables share one discipline: every table cleanly separates canonical data from derived serving state. The source of truth lives in the parent table, and hot-path denormalization lives in a matching *_state table. borg_entities holds the canonical graph nodes; borg_entity_state holds the tier, salience, and access counters that change constantly during serving. borg_facts holds the temporal edges; borg_fact_state holds their salience and access data.

This split is what lets one database be both the system of record and the fast serving layer without those two jobs corrupting each other. The canonical tables stay clean and rarely change. The state tables absorb the churn. Neither pretends to be the other.

Transactions are the underrated win

When memory lives in one database, a write is atomic. Recording an episode, extracting its facts, and updating the graph commit coherently. There is no window where the graph knows about an entity the fact table hasn’t heard of yet.

Split that across Qdrant and Neo4j and atomicity is gone. You’re left writing compensating logic for partial failures, which is exactly the kind of code that looks fine in review and corrupts state in production six weeks later. ACID transactions keeping mutations coherent is not a nice-to-have here, it’s the whole reason the single-store design works.

It runs anywhere Postgres runs

A practical consequence: deployment is boring, in the good way. Borg runs on managed Postgres, self-hosted, or a local instance, minimum version 14. There’s no separate graph cluster to size, no vector service to scale independently. The thing your team already knows how to operate is the thing the memory system runs on.

For most organizations, “it’s just another Postgres database” is a sentence that ends a procurement conversation rather than starting one.

The honest tradeoff

This isn’t free. A purpose-built graph database will out-traverse Postgres on certain pathological graph workloads, and a dedicated vector store can be tuned for scales Postgres has to work harder to reach. Borg’s own position is that a dedicated graph DB is a measured-bottleneck escape hatch, not a starting point: if traversal genuinely becomes the bottleneck at your scale, that’s when you reach for one.

But that is not the shape of coding-agent memory. The expected scale is hundreds of entities and thousands of facts, the traversals are one or two hops, and the access pattern is mixed graph-vector-relational, which is precisely where one engine beats three. The specialized-database advantage is real and, for this workload, premature.

So the case for one Postgres is not that Postgres is best at everything. It’s that for this specific job, a single transactional engine you already trust beats a synchronized stack you have to babysit.

The next post goes deep on the single capability that depends most on having one consistent store: keeping stale, contradicted facts out of the agent’s context.


Part 3 of a 6-part series on building memory for AI coding agents. Next: Fixing stale memory with temporal facts and supersession.

Architecture depth + engineering leadership

Building platforms enterprises trust at scale.