JanusGraph is an open-source distributed graph database that runs on top of an existing wide-column store (Apache Cassandra, ScyllaDB, HBase, or BerkeleyDB) for storage and an external indexing system (Elasticsearch, Solr, or Lucene) for full-text and geo queries. JanusGraph is the spiritual successor to Titan, forked by IBM, Google, Hortonworks, and others in 2017. It uses Apache TinkerPop’s Gremlin as its query language and is the standard choice when Neo4j’s single-node scale is insufficient.
JanusGraph is an open-source distributed graph database under the Linux Foundation, forked from Titan in 2017. It does not implement its own disk storage. Instead it maps the property-graph model onto a wide-column or key-value store, and delegates full-text, geo and range queries to an external search engine. Queries are written in Gremlin, the traversal language of Apache TinkerPop, and are executed either embedded in a JVM application or through JanusGraph Server, which is a configured Gremlin Server.
This design means JanusGraph scales storage horizontally to the extent the backend does (Cassandra and ScyllaDB scale to very large clusters), and it inherits that backend's consistency and operational characteristics. You operate two or three distributed systems rather than one.
Each vertex becomes one row keyed by its ID. The row's columns hold the vertex properties and every incident edge, sorted by edge label and direction, with the adjacent vertex ID and edge properties encoded in the cell. Fetching a vertex's neighbours is therefore a single row read, and a traversal step is a sequence of row lookups. Vertex-centric indexes sort a vertex's edges by a property so that "the 10 most recent follows" does not have to scan all edges of a high-degree vertex. Composite indexes (exact match, stored in the storage backend) and mixed indexes (in Elasticsearch or Solr) support finding starting vertices by property.
// Gremlin console (Groovy syntax)
graph = JanusGraphFactory.open('conf/janusgraph-cql-es.properties')
mgmt = graph.openManagement()
name = mgmt.makePropertyKey('name').dataType(String.class).make()
person = mgmt.makeVertexLabel('person').make()
knows = mgmt.makeEdgeLabel('knows').multiplicity(MULTI).make()
mgmt.buildIndex('byName', Vertex.class).addKey(name).buildCompositeIndex()
mgmt.commit()
g = graph.traversal()
a = g.addV('person').property('name', 'ana').next()
b = g.addV('person').property('name', 'bo').next()
c = g.addV('person').property('name', 'cy').next()
g.addE('knows').from(a).to(b).iterate()
g.addE('knows').from(b).to(c).iterate()
graph.tx().commit()
// friends of friends of ana, excluding ana
g.V().has('person', 'name', 'ana').out('knows').out('knows')
.where(neq(a)).dedup().values('name') // ==> cy
// shortest path between two people (up to 4 hops)
g.V().has('name', 'ana').repeat(out('knows').simplePath())
.until(has('name', 'cy')).limit(1).path().by('name')
Defining the schema explicitly and creating the composite index before loading data matters: without an index, has('name', 'ana') becomes a full graph scan, and JanusGraph logs a warning (or rejects the query if force-index is enabled).
| System | Model | Choose when |
|---|---|---|
| JanusGraph | Property graph over Cassandra/HBase/ScyllaDB | Very large graphs, you already run those backends, open source required |
| Neo4j (comparison) | Native graph storage, Cypher | Fast deep traversals, strong tooling, single-cluster scale |
| Amazon Neptune | Managed, Gremlin/openCypher/SPARQL | AWS shop wanting no operations |
| Dgraph | Native distributed graph, GraphQL/DQL | GraphQL-first applications |
Because every hop is a backend read over the network, JanusGraph's per-hop latency is higher than a native graph engine that follows in-memory pointers. It favours wide, shallow traversals on huge graphs and bulk analytics through Spark (SparkGraphComputer) over sub-millisecond deep traversals.
both() steps on it.