Store the DAG in plain PostgreSQL tables, not a graph database
Context and Problem Statement
Skift’s domain is a directed acyclic graph of tasks. Does a graph shaped domain need a graph database, or a graph extension on PostgreSQL, or is a relational schema sufficient at the target scale of a thousand daily active users and hundreds of concurrent agents?
Decision Drivers
- The hot path is the ready set and claims, not traversal
- One shared PostgreSQL already exists in the platform zone; a second datastore is operational cost with no consumer
- Concurrency correctness for claims matters more than query expressiveness
- sqlc and goose are the chosen data tooling and both assume SQL
Considered Options
- Plain tables with recursive CTEs
- PostgreSQL with the Apache AGE extension (Cypher)
- A dedicated graph database such as Neo4j
Decision Outcome
Chosen option: plain tables with recursive CTEs, because every query skift needs is either a one-hop anti-join or a bounded recursive query over small, shallow components, and claims are the well-known job-queue pattern that PostgreSQL row locking handles directly.
Consequences
- Good: Readiness is an anti-join on an indexed edge target, scoped by project. Microseconds.
- Good:
FOR UPDATE SKIP LOCKEDgives correct, contention-free claims under hundreds of concurrent orchestrators, as used by River, Oban, and graphile-worker. - Good: Cycle detection and subtree rollups are recursive CTEs bounded by a connected component that is rarely deeper than 20 or wider than a few thousand nodes.
- Good: Transactions make derived views consistent under concurrent mutation, which is harder in most graph stores.
- Bad: Variable-length pattern matching across millions of edges would be slow. Skift has no such query.
- Neutral: Every query must be tenant and project scoped so
Readynever scans across organizations. That is required regardless of engine. - Neutral: The event table is time-partitioned from the first migration.
Rejected Options
Apache AGE
- Good: Cypher on the same PostgreSQL.
- Bad: An extension to build, operate, and upgrade. Maturity is uneven. Poor fit with sqlc.
Dedicated graph database
- Good: Best for whole-graph algorithms and deep pattern matching.
- Bad: Second datastore, second set of credentials, weaker transactional story with the rest of the data, no need at target scale.
More Information
Revisit if a required query becomes a variable-length path search across a component larger than tens of thousands of nodes, or if SQL/PGQ lands in PostgreSQL and simplifies the recursive queries.