TypeDB vs Neo4j: how to compare them

A practical comparison of TypeDB and Neo4j: where the two differ, when each one fits, and how to test both on your own data.

Cal Hemsley


If you’re evaluating graph databases, you’re almost certainly looking at both TypeDB and Neo4j. Both are robust systems for connected data, built on very different ideas about how that data should be modelled. Those differences decide which one fits your problem, and this post sets out what they are and how to test them.

Neo4j is a property graph built around fast traversal, with a flexible model that lets you start loading data straight away. TypeDB is a typed hypergraph, where a relation can connect any number of things and a schema defines what each piece of data is and how it can connect.

Typed graphs are becoming the norm. Neo4j’s graph types, now in Enterprise Edition, are one example, and we’ve written about the wider trend in Everyone is making structured graphs now. TypeDB has been built around a typed model from the start.

Summary

TypeDBNeo4j
Data modelEntities, relations and attributes, all typedNodes, relationships and properties
RelationshipsN-ary, with named roles. Relations can take part in other relationsBinary, directed edges between two nodes
TypesHierarchies with inheritance across entities, relations and attributesLabels on nodes, and one type per relationship
SchemaRequired, and can start smallOptional. Constraints and graph types can be added
Error checkingWhen the schema is written, when data is written, and when queries are writtenWhen data is written, against whatever constraints or graph type you’ve defined. Queries warn on unknown labels and properties
Query languageTypeQLCypher
Strongest atComplex domains where the model has to be rightDeep traversal, pathfinding, getting started quickly

Relationships

  • Neo4j: Relationships are edges between two nodes, so anything with three or more participants becomes a node.
  • TypeDB: Relations can have any number of participants, each in a named role, and can take part in other relations.

Every relationship in Neo4j connects exactly two nodes. In scenarios where a relationship involves more than two parties, there are work-arounds to address this. The usual approach is to make the concept a node (reification) and hang relationships off it:

MATCH (b:Company {name: 'Acme'}),
      (s:Company {name: 'Globex'}),
      (k:Brokerage {name: 'Initech'})
CREATE (c:Contract {signedDate: date('2026-03-01')}),
       (b)-[:BUYER_IN]->(c),
       (s)-[:SELLER_IN]->(c),
       (k)-[:BROKER_IN]->(c)

Neo4j’s graph types can also check that a BUYER_IN relationship starts at a :Company and ends at a :Contract. Even so, the database doesn’t know that a contract is made up of a buyer, a seller and a broker. That only exists in your application code.

In TypeDB the contract is a relation, and its roles are part of the schema:

define
  attribute signed-date, value date;
  relation contract,
    relates buyer,
    relates seller,
    relates broker,
    owns signed-date;
  entity company, plays contract:buyer, plays contract:seller;
  entity brokerage, plays contract:broker;
match
  $b isa company, has name "Acme";
  $s isa company, has name "Globex";
  $k isa brokerage, has name "Initech";
insert
  $c isa contract, links (buyer: $b, seller: $s, broker: $k),
    has signed-date 2026-03-01;

A relation can own attributes. Neo4j relationships have properties, so that much is similar. However, a TypeDB relation can also play a role in another relation. If the contract ends up disputed, the dispute is its own relation, with the contract as one of its participants:

define
  relation dispute, relates subject, relates claimant;
  relation contract, plays dispute:subject; ## nested relation!
  entity company, plays dispute:claimant;

In a property graph you’d reify again. Each layer adds more intermediate nodes that the database treats as ordinary data, so more of the rules about how they fit together end up in application code. The result is schema drift, where the model the team intends and the data in the graph gradually stop matching.

Type hierarchies

  • Neo4j: Nodes can carry multiple labels, and in Enterprise Edition one label can require another. Each relationship has exactly one type.
  • TypeDB: Entities, relations and attributes all form type hierarchies, and a query on a parent type returns its subtypes.

Neo4j lets a node carry several labels, so a listed company can be :Company:PublicCompany. With graph types in Cypher 25 you can also require that anything labelled :PublicCompany carries :Company, which covers the node side reasonably well. Relationships are different. Each one has exactly one type, so a SALE_CONTRACT can’t be declared a kind of CONTRACT. Anything that needs a hierarchy usually becomes a node instead, as the contract did in the previous section.

In TypeDB, entity, relation and attribute types can all form hierarchies:

define
  attribute name, value string;
  attribute market-id @abstract;
  attribute ticker sub market-id, value string;
  attribute isin sub market-id, value string;
  entity company @abstract, owns name,
    plays contract:buyer, plays contract:seller;
  entity public-company sub company, owns ticker, owns isin;
  entity private-company sub company;
  relation contract @abstract, relates buyer, relates seller;
  relation sale-contract sub contract;
  relation lease-contract sub contract;

Represented some of these concepts visually:

ENTITY company public-company private-company RELATION contract sale-contract lease-contract ATTRIBUTE market-id ticker isin

Ask for companies and you get both kinds without listing them:

match
  $c isa company, has name $n;
fetch {
  "name": $n
};

Add a partnership subtype later and the same query returns it, unchanged. Relations and attributes inherit the same way, so contract can be split into sale-contract and lease-contract, and a query for contracts still returns both, roles included.

Validation and errors

  • Neo4j: Data is accepted by default. Constraints, and graph types in Enterprise Edition, enforce rules for the parts of the graph they cover.
  • TypeDB: The schema is checked when written, data is checked against it at commit, and queries are type-checked against it.

Neo4j is permissive by default. Out of the box, nothing stops you creating a MARRIED_TO relationship between a User and a Vehicle.

Graph types, an Enterprise Edition feature, can set the labels a relationship’s source and target nodes must have, so the married vehicle is rejected. They’re open, though, so anything they don’t describe can still be written.

Alongside uniqueness, existence, key and property type constraints, Cypher 25 added graph types, which set the labels a relationship’s source and target nodes must have. With one in place, the married vehicle is rejected. Graph types are open, though, so anything they don’t describe can still be written, and coverage depends on how much of the graph someone has typed.

TypeDB checks at three points.

  • When you write the schema, TypeDB checks it for consistency. Roles must be played by types that exist, and a subtype can’t contradict its parent.
  • When you write data, anything that doesn’t fit the schema is rejected at commit, so your schema and your data never drift apart.
  • When you write a query, TypeDB works out from the schema which types each variable could be.

That last check goes further than Cypher’s. Cypher warns you about labels or properties that don’t exist anywhere in the database. TypeDB also checks combinations. If you ask for the engine size of a person, and both people and engine sizes exist, you get an error, because the schema doesn’t allow a person to own an engine size. This catches a common class of production bugs, since a broken query that returns an empty result looks exactly like a correct one.

Schema first vs schema optional

  • Neo4j: No schema is needed to start. Unenforced concepts are written to application code.
  • TypeDB: A schema is required first, but it can start small and grow.

Neo4j lets you load data straight away and add constraints or a graph type later, if you decide you want them. That makes it quick to start exploring a dataset you don’t know well yet.

Any rule the database doesn’t enforce still has to be enforced somewhere, usually in application code. That means more code, more places to validate, and checks that have to handle concurrent writes without the transactional guarantees the database would otherwise provide.

TypeDB requires a schema before loading data, so someone decides up front what your data actually represents.

Beyond what that does for the model itself, it takes less effort than people expect, for two reasons.

  • The schema doesn’t have to be finished before you start. You can build it up incrementally and in modules, beginning with a handful of entity and relation types and adding hierarchy and roles as the picture firms up.
  • Extending it is a routine operation, and the database checks each change against the data already there.

In exchange for describing your data model to TypeDB, you get a model the database enforces, which also serves as documentation for anyone working with the data.

Extending the model

  • Neo4j: Adding labels and properties is immediate. Changing a relationship’s shape usually means migrating data and queries.
  • TypeDB: New roles and subtypes are additive, so existing data and queries keep working.

Adding something new to a Neo4j graph is immediate. You write a node with a new label or a new property, and it’s there. In TypeDB you add it to the schema first, with a define statement.

The difference is larger when an existing relationship needs to change shape. Say you’ve modelled employment as an EMPLOYED_BY edge between a person and a company, and later need to record which department the job sits in. The department is another participant in the employment, so in a property graph the edge has to become a node. That means migrating the existing data and rewriting the queries that traverse it.

In TypeDB the relation takes a new role:

define
  relation employment, relates department;
  entity department, plays employment:department;

Existing employments stay valid without a department, and existing queries return the same results. Subtypes behave the same way. Because queries are written against the parent type, a new partnership subtype appears in their results with no changes to the query.

This matters most in enterprise deployments. When a change is additive, the team that owns the data can extend the schema and load new data while the applications built on top keep running unchanged. Each application can adopt the new structure when it’s ready. Not every change is additive. Renaming types or restructuring a hierarchy still needs a migration. Over the life of a system the model will be extended many times, so the cost of each extension adds up.

Path queries

  • Neo4j: Variable-length paths are built into Cypher and are concise to write.
  • TypeDB: Paths of unknown length need a recursive function.

Finding everyone within five hops of a person is one line of Cypher:

MATCH (p:Person {name: 'Alice'})-[:KNOWS*1..5]-(other:Person)
RETURN DISTINCT other.name

In TypeDB you’d write a recursive function that follows the relation to the depth you need. It works, but it is more code, and for workloads built around open-ended path queries Neo4j is the more natural fit.

Performance

  • Neo4j: Faster on small, in-memory datasets.
  • TypeDB: In our TPC-C benchmark, held throughput better as data grew beyond memory.

We ran TPC-C against TypeDB CE 3.4.1 and Neo4j CE 5.26.5, growing the dataset from 1 to 400 warehouses, running single-threaded on an 8-core 16 GB machine. The full write-up is in First look at TypeDB 3 benchmarks. TPC-C is a transactional benchmark rather than a graph one, so it tells you nothing about deep traversal, where Neo4j is the stronger choice

Graph showing workloads completed in 10 minutes for TypeDB CE and Neo4J CE as the dataset size increases. TypeDB CE shows higher completion rates compared to Neo4J CE, with a performance drop as the number of warehouses increases.

On the smallest dataset Neo4j was about two and a half times faster. By 100 warehouses, with the data well past memory, TypeDB was around 14% ahead. From there to 400 warehouses Neo4j’s throughput fell by about half and TypeDB’s by about a quarter, so at 400 TypeDB was completing around 75% more workloads.

Bar chart comparing 99th percentile latency by workload at 400 warehouses, showing metrics for TypeDB CE 3.4.1 and Neo4j CE 5.26.5.

Tail latency was more mixed. Neo4j was faster on two of the five workloads. On the other three, TypeDB’s 99th percentile latency was between a quarter and a third of Neo4j’s, and its slowest transaction stayed under 200 ms, while Neo4j’s reached several seconds.

We always recommend benchmarking both systems with a sample of your real data, to see how each handles the complexity of your workloads.

Which to use when

It depends a lot on what you’re building, and we’re happy to help with the comparison for your use case. As a general guide, Neo4j is likely the better fit if:

  • your model is fairly simple and unlikely to get much more complicated
  • the core workload is traversal, pathfinding or graph analytics
  • you want to load data before agreeing a schema
  • your team already knows Cypher, or you’d prefer an established product with a large ecosystem and mature operational tooling

TypeDB is likely the better fit if:

  • your domain model is complex, with many entity and relation types that depend on each other
  • you work in a regulated domain and need to make sure the shape of your data doesn’t drift over time
  • relationships involve more than two participants, each in a distinct role, or need to nest to add layers of context and annotation
  • you want the database to catch invalid data and invalid queries

If your system has a bit of both, which is common, it’s worth trying each. Model the most awkward part of your domain in both and see which you’d rather maintain.

Running a formal evaluation

Neo4j is likely to feature in any formal evaluation. We’d suggest running TypeDB alongside it on the same test case, because most of the differences above are most obvious using your own data.

Using your test case, compare:

  • how closely each model matches the way your team talks about the domain
  • what happens when you write data that shouldn’t be allowed, and when you write a query that can’t be right
  • how much of the logic ends up in the database and how much in application code
  • the time it took to build the TypeDB schema, compared with the time to encode the same validation in the application layer
  • how each performs on the queries you need, at the data size you expect to run

Neo4j is quick and easy to start with. For complex models that will keep growing, we find TypeDB pays off.

TypeDB is open source, so you can try it today. If you’re running an evaluation and want help setting up the TypeDB side, get in touch.

Share this article

TypeDB Newsletter

Stay up to date with the latest TypeDB announcements and events.

Subscribe to newsletter

Further Learning

Feedback