SQLite is everywhere. It's in your phone, your browser, your smart TV, probably your car. It solved a fundamental problem — giving applications a full SQL database without running a separate server — and it did it so well that 'embedded database' and 'SQLite' became almost synonymous. But SQLite's relational model isn't always the right fit. If your data is fundamentally about relationships — social connections, dependency graphs, knowledge networks, routing problems — forcing it into tables with foreign keys creates queries that are either hideously complex or painfully slow.
A new wave of embeddable graph databases is trying to do for graph data what SQLite did for relational data: give you a fast, dependency-free, in-process database that speaks the right query language for connected data. Several of these are written in Rust, which turns out to be an excellent fit for the problem. Let's look at why graph databases are going embeddable, what the Rust ecosystem brings to the table, and when you should actually consider one.
The Relational Model's Blind Spot
Relational databases handle most data patterns well. One-to-many? Foreign key. Many-to-many? Junction table. Simple lookups, aggregations, filters — SQL was built for this. The trouble starts when your queries care about paths, depth, and connectivity.
Consider a dependency resolver. You have packages, each depending on other packages, each with version constraints. You need to answer: 'If I install package X, what's the full transitive dependency tree? Are there any circular dependencies? Are there conflicting version requirements at any depth?' In SQL, this requires recursive CTEs (common table expressions), which are verbose, hard to optimize, and get exponentially slower as the graph gets deeper.
-- Finding all transitive dependencies in SQL
-- This works, but gets ugly fast
WITH RECURSIVE deps AS (
-- Base case: direct dependencies
SELECT dependency_id, 1 as depth
FROM package_dependencies
WHERE package_id = 'my-package'
UNION ALL
-- Recursive case: dependencies of dependencies
SELECT pd.dependency_id, d.depth + 1
FROM package_dependencies pd
JOIN deps d ON pd.package_id = d.dependency_id
WHERE d.depth < 50 -- safety limit to prevent infinite loops
)
SELECT DISTINCT dependency_id, MIN(depth) as min_depth
FROM deps
GROUP BY dependency_id
ORDER BY min_depth;
-- Now try detecting circular dependencies.
-- Or finding the shortest path between two packages.
-- Or querying all packages within 3 hops that match a version constraint.
-- Each query gets progressively more painful.
In a graph database, this is the native query pattern. You're not fighting the data model — you're working with it. Traversing edges, following paths, detecting cycles: these are first-class operations, not bolted-on recursion.
Why Embeddable Matters
Neo4j has been the dominant graph database for over a decade, and it's genuinely excellent. But it's a server. You run it as a separate process, connect over a network protocol, manage its JVM memory, and handle its operational overhead. For a production application with a dedicated ops team, that's fine. For a CLI tool, a desktop app, a build system, or an embedded device, it's absurd.
The SQLite insight applies here: many use cases need graph query capabilities without server overhead. A code analysis tool that builds a call graph. A game engine that stores entity relationships. A local-first note-taking app with bidirectional links. A network topology analyzer. All of these want graph queries, none of them want to tell their users to install and configure a database server.
Embeddable graph databases run in your process, store data in local files, and expose a library API instead of a network protocol. Your application links against them like it would link against SQLite. No server, no ports, no authentication, no deployment complexity.
What Rust Brings to Graph Databases
Rust has become the language of choice for a disproportionate number of new database projects, and the reasons go beyond the usual 'memory safety without garbage collection' pitch.
- Predictable latency. Graph traversals are latency-sensitive. Each hop in a traversal is a memory access, and deep traversals do millions of them. A GC pause in the middle of a 10-hop traversal blows your tail latency. Rust's ownership model gives you deterministic memory management without GC pauses — critical for consistent query performance.
- Safe concurrency. Graph databases benefit enormously from parallel traversal. Exploring multiple paths simultaneously can turn a 100ms query into a 10ms one. Rust's type system prevents data races at compile time, which means you can parallelize aggressively without fear of corrupting your graph data.
- Small binary, no runtime. For an embeddable database, deployment size matters. A Rust graph database compiles to a single native library with no runtime dependencies. Compare this to a JVM-based solution that needs a 200MB runtime, or a Go solution that bundles a garbage collector you didn't ask for.
- C FFI. Rust's ability to expose a C-compatible API means the database can be used from virtually any language. Write it in Rust, call it from Python, JavaScript, Go, Swift, or anything else that speaks C.
The Property Graph Model
Most embeddable graph databases use the property graph model, which is worth understanding if you haven't worked with graph databases before. The model has three primitives:
- Nodes — entities with a label and key-value properties. Think of them as rows in a table, but without a fixed schema.
- Edges — directed connections between nodes, also with a label and properties. The label describes the relationship type (DEPENDS_ON, AUTHORED_BY, LINKS_TO).
- Traversals — queries that follow edges from node to node, optionally filtering by properties, aggregating results, or finding paths.
// Conceptual API for an embeddable graph database
let db = GraphDB::open("my_graph.db")?;
// Create nodes
let alice = db.create_node("Person", props! {
"name" => "Alice",
"role" => "engineer"
})?;
let project = db.create_node("Project", props! {
"name" => "Auth Service",
"language" => "Rust"
})?;
// Create edges
db.create_edge(alice, project, "WORKS_ON", props! {
"since" => "2025-01"
})?;
// Traverse: find all projects worked on by Alice's teammates
let results = db.traverse()
.from(alice)
.follow("WORKS_ON") // Alice -> projects
.reverse("WORKS_ON") // projects <- other people
.follow("WORKS_ON") // other people -> their projects
.filter(|node| node.label() == "Project")
.collect()?;
The property graph model is more flexible than relational schemas for rapidly evolving data. You don't need to define your schema upfront or run migrations when you add a new relationship type. Just create edges with new labels. This schemaless flexibility is a double-edged sword — you lose the safety guarantees of a well-defined schema — but for applications where the graph structure evolves (knowledge bases, social networks, dependency tracking), it's a pragmatic trade-off.
When to Actually Use a Graph Database
Graph databases get recommended in situations where they're unnecessary, and overlooked in situations where they'd genuinely help. Here's my honest assessment of where they shine and where they don't.
Strong fit: Dependency resolution, access control (who can access what through which group memberships), fraud detection (finding connections between entities), recommendation engines (users who liked X also liked Y), network topology analysis, knowledge graphs, and code analysis tools. The common thread: your queries naturally express paths and connectivity.
Weak fit: Simple CRUD applications, time-series data, analytics/aggregation workloads, anything where your queries are primarily 'fetch records matching condition X.' If you're doing SELECT * FROM users WHERE country = 'US' ORDER BY created_at, a relational database is the right tool. Don't use a graph database because it sounds interesting — use it because your queries are genuinely graph-shaped.
The litmus test I use: draw your data model on a whiteboard. If it's mostly boxes in rows (entities with attributes), use a relational database. If it's boxes connected by arrows and the arrows matter as much as the boxes, consider a graph database.
Storage Engines and Trade-offs
Under the hood, embeddable graph databases face interesting storage engine decisions. The most common approaches:
- Adjacency list storage. Each node stores a list of its outgoing edges. Fast for local traversals (finding a node's neighbors) but slow for global queries (find all edges with label X). Most embeddable graph databases use this because local traversals are the most common operation.
- Edge-list storage. Edges are stored in a separate sorted structure, indexed by source, target, or label. Better for global queries and bulk operations, but adds indirection to local traversals.
- Hybrid approaches. Some databases use adjacency lists for traversal and maintain secondary indexes on edge labels or node properties for filtered queries. This is the most flexible but uses more storage and makes writes more expensive.
Many Rust-based graph databases build on top of existing embedded key-value stores like RocksDB or sled. This is a pragmatic choice — you get battle-tested persistence, crash recovery, and compaction for free. The graph layer maps nodes and edges to key-value operations. The downside is that your performance ceiling is bounded by the key-value store's characteristics, and graph-specific optimizations (like storing a node's edges contiguously on disk for cache-friendly traversal) can be harder to implement.
Query Languages: The Unsettled Question
SQL is the universal language for relational databases. Graph databases don't have an equivalent consensus. Cypher (from Neo4j), Gremlin (from Apache TinkerPop), SPARQL (for RDF graphs), and the emerging GQL standard all compete for mindshare.
Most embeddable graph databases sidestep this by offering a builder-pattern API in the host language rather than a query language. You construct traversals with method chains, which is ergonomic in typed languages and avoids the complexity of parsing and optimizing a query language. The trade-off is that your queries aren't portable between databases — switching from one embeddable graph database to another means rewriting your query code.
GQL (Graph Query Language) is the ISO standard that's supposed to unify graph querying. It borrows heavily from Cypher and is gradually gaining adoption. If you're choosing an embeddable graph database today, it's worth checking whether it has GQL support on the roadmap — standardized query languages tend to win in the long run, even if proprietary ones are more polished initially.
Practical Considerations
If you're evaluating an embeddable graph database for your project, here's what actually matters in practice:
- Measure with your workload. Graph database benchmarks are notoriously misleading. A database that excels at shallow, wide traversals (social network friend-of-friend) may struggle at deep, narrow ones (dependency chain resolution). Prototype with your actual query patterns.
- Check the crash safety story. Embedded databases live and die by their durability guarantees. Does the database use write-ahead logging? Is it crash-safe? Can it recover from a power failure without data loss? 'Corruption on unexpected shutdown' is not an acceptable answer for production use.
- Look at the memory model. Some embeddable graph databases memory-map the entire graph, which works great until your graph exceeds available RAM. Others use a disk-first approach with caching. Know which model your chosen database uses and whether your graph fits.
- Consider the binding quality. If the database is written in Rust but you're using it from Python, the Python bindings matter more than the Rust internals. Check whether the bindings are maintained, documented, and performant — or an afterthought.
- Think about migrations. Schemaless doesn't mean change-free. When your graph model evolves, how do you handle existing data? Some databases support migration scripts or versioned schemas. Others leave it entirely to you.
The embeddable graph database space is still young compared to the relational world. SQLite has been refined for over two decades. Most embeddable graph databases are under five years old. That means rougher edges, fewer community resources, and more risk. But the core value proposition — graph queries without server overhead — is sound. For the right use case, an embeddable graph database can replace hundreds of lines of recursive SQL with a few lines of traversal code, run faster while doing it, and make your data model actually match the problem you're solving.