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.TypeDB vs Neo4j

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.

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

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 contract 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, an Enterprise Edition feature, 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 how you’ve named things, and 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 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;

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 is permissive by default. Out of the box, nothing stops you creating a MARRIED_TO relationship between a User and a Vehicle.

This has changed recently. 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 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.

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 three 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.
  • 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.

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

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.

Performance

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

If you’re running a formal evaluation, Neo4j will almost certainly be in it, and it should be. 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. [Get started link] 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