Anyone arriving at RDF from Neo4j asks the same question. Anyone arriving at Neo4j from RDF asks the inverse. Which one should I be using? What is the actual difference?
Most existing answers are partisan. Vendor pieces lean one way. Semantic-web defenders lean the other. The honest read is that both stacks are correct for their context, and the choice has cascading consequences for anyone designing a real system. Pick wrong once and the next project will ask the question better.
This page tries to help you ask it the first time. The comparison gets concrete in Section 3, where the same small resume gets modeled in both. The conceptual unlock arrives in Section 4, where one query — the one that follows a skill into the ESCO occupational graph — exposes what each stack actually buys you.
RDF and labeled property graphs agree on more than their advocates suggest. Both describe a world of entities and the relationships between them. They diverge on where information about the relationship lives.
In RDF, every fact is a triple: subject, predicate, object. Subjects and predicates are URIs. Objects are URIs or literals. That is the entire data model. Classes, vocabularies, ontologies, reasoning — every higher-order structure is convention layered on top of triples.
The bare assertion "Alice knows Bob" is one triple:
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
:Alice foaf:knows :Bob .
The predicate foaf:knows is a URI from the FOAF vocabulary. Anyone else's graph that uses foaf:knows means the same relationship. That global agreement is the entire point.
In a labeled property graph, nodes carry labels and properties. Edges carry types and properties. Both sides of the connection have their own attribute dictionary. Schema is local; the database does not try to make your KNOWS edge mean the same thing as anyone else's.
The same assertion in Cypher:
CREATE (alice:Person)-[:KNOWS]->(bob:Person)
Two nodes labeled Person, one relationship typed KNOWS. Clean.
Add a property to the relationship — "they have known each other since 2020." In Cypher, the property goes on the edge directly:
CREATE (alice:Person)-[:KNOWS {since: 2020}]->(bob:Person)
In Turtle, predicates cannot carry properties. The metadata-about-the-triple has to live somewhere else. The standard options: turn the relationship into an event class with its own properties, use RDF-star to annotate the triple itself, or apply classical reification. Module 3 covers the trade-offs in depth; for now, name the cost.
:Alice :hasFriendship [
a :Friendship ;
:with :Bob ;
:startDate "2020-01-01"^^xsd:date
] .
Three lines became six. The same fact, more characters, more nesting. This is the URI tax in its smallest form, and it shows up everywhere in the rest of the page.
One small, realistic resume in both data models. Alex Rivera, two jobs in the data field, a statistics undergrad, three skills. Small enough to fit on screen, structured enough to show the choices each model forces you to make.
// People, organizations, schools CREATE (alex:Person {name: "Alex Rivera", email: "alex@example.com"}) CREATE (riverbend:Company {name: "Riverbend Analytics"}) CREATE (northwind:Company {name: "Northwind Health"}) CREATE (midstate:School {name: "Midstate University"}) // Employment relationships, with properties on the edge CREATE (alex)-[:EMPLOYED_BY { role: "Junior Data Analyst", startDate: date("2020-01-15"), endDate: date("2022-06-30") }]->(riverbend) CREATE (alex)-[:EMPLOYED_BY { role: "Data Scientist", startDate: date("2022-07-01") }]->(northwind) // Education CREATE (alex)-[:EDUCATED_AT { degree: "BS Statistics", endDate: date("2019-05-15") }]->(midstate) // Skills, with ESCO IDs as string properties CREATE (python:Skill {name: "Python (programming)", escoId: "ccd0a1d9-..."}) CREATE (sql:Skill {name: "SQL", escoId: "29cdd2a9-..."}) CREATE (ml:Skill {name: "Machine learning", escoId: "4d4d5c8e-..."}) CREATE (alex)-[:HAS_SKILL {level: "advanced"}]->(python) CREATE (alex)-[:HAS_SKILL {level: "advanced"}]->(sql) CREATE (alex)-[:HAS_SKILL {level: "intermediate"}]->(ml)
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . @prefix foaf: <http://xmlns.com/foaf/0.1/> . @prefix schema: <https://schema.org/> . @prefix skos: <http://www.w3.org/2004/02/skos/core#> . @prefix sensemaking: <https://sensemaking-ai.com/ns/> . # ESCO IRIs shown are illustrative. :alex a foaf:Person ; foaf:name "Alex Rivera" ; schema:email "alex@example.com" ; sensemaking:hasEmployment :emp1, :emp2 ; sensemaking:hasEducation :edu1 ; sensemaking:hasSkill :s_python, :s_sql, :s_ml . :emp1 a sensemaking:Employment ; sensemaking:employer :riverbend ; sensemaking:role "Junior Data Analyst" ; sensemaking:startDate "2020-01-15"^^xsd:date ; sensemaking:endDate "2022-06-30"^^xsd:date . :emp2 a sensemaking:Employment ; sensemaking:employer :northwind ; sensemaking:role "Data Scientist" ; sensemaking:startDate "2022-07-01"^^xsd:date . :edu1 a sensemaking:Education ; sensemaking:institution :midstate ; sensemaking:degree "BS Statistics" ; sensemaking:endDate "2019-05-15"^^xsd:date . :riverbend a schema:Organization ; schema:name "Riverbend Analytics" . :northwind a schema:Organization ; schema:name "Northwind Health" . :midstate a schema:CollegeOrUniversity ; schema:name "Midstate University" . :s_python a skos:Concept ; skos:prefLabel "Python (programming)" ; skos:exactMatch <http://data.europa.eu/esco/skill/ccd0a1d9-illustrative> . :s_sql a skos:Concept ; skos:prefLabel "SQL" ; skos:exactMatch <http://data.europa.eu/esco/skill/29cdd2a9-illustrative> . :s_ml a skos:Concept ; skos:prefLabel "Machine learning" ; skos:exactMatch <http://data.europa.eu/esco/skill/4d4d5c8e-illustrative> .
Three things to notice.
First, dates on the employment relationship. Cypher attaches them to the edge with native property syntax. Turtle cannot; the standard option is to model employment as its own thing — here, sensemaking:Employment — with the dates as properties of that event. This is the first taste of reification, which Module 3 covers in depth. For now, name the decision. In RDF, attaching properties to a relationship requires modeling the relationship as something.
Second, the skills. Both versions name them, but the Turtle version types each skill as skos:Concept and adds skos:exactMatch to its ESCO concept IRI. In Cypher, the same data lives as a string property; the ESCO link is a label, not a hyperlink the database knows how to traverse. The difference looks cosmetic. Section 4 shows it isn't.
Third, line count. Turtle ends up around 35 lines; Cypher around 25. The verbosity is real and not going away. The next two sections show what the verbosity buys and where it does not earn its keep.
The Turtle prefix block above is the curriculum standard. Full reference: Module 1 cheat sheet, Section 1.
Three questions you would actually ask of this resume. Each one in both languages, each one answered honestly about which stack pulls ahead.
Cypher:
MATCH (p:Person {name: "Alex Rivera"})-[r:EMPLOYED_BY]->(c:Company) RETURN c.name, r.role, r.startDate ORDER BY r.startDate
SPARQL:
PREFIX schema: <https://schema.org/> PREFIX sensemaking: <https://sensemaking-ai.com/ns/> SELECT ?orgName ?role ?start WHERE { :alex sensemaking:hasEmployment ?emp . ?emp sensemaking:employer ?org ; sensemaking:role ?role ; sensemaking:startDate ?start . ?org schema:name ?orgName . } ORDER BY ?start
Cypher wins on ergonomics. Path traversal is what Neo4j was built for; the single MATCH pattern reads like a sentence. The SPARQL version is heavier — the Employment events are nodes, joined to the person via sensemaking:hasEmployment, joined again to the organization via sensemaking:employer. Both queries work. One feels lighter.
This is where the stacks separate.
In Cypher, the resume does not know what ESCO is. Each skill carries an ESCO string ID as a property, but the database has no idea what that string points to. To return "broader categories," you would need to import a copy of the ESCO graph as a separate set of :ESCOConcept nodes, maintain that copy as ESCO updates, and join across the string ID — or step outside the database, look up ESCO yourself, and bring the answer back in.
MATCH (p:Person {name: "Alex Rivera"})-[:HAS_SKILL]->(s:Skill) // To return broader ESCO categories, you would need an :ESCOConcept // subgraph already loaded and joined by escoId. ESCO is external // to this database. RETURN s.name, s.escoId
In SPARQL, ESCO is published as RDF. Each skill's skos:exactMatch link points to a real skos:Concept whose skos:broader edges connect to the ESCO occupational hierarchy directly. Following those edges is a normal query — no import, no glue code, no joins on string IDs.
PREFIX skos: <http://www.w3.org/2004/02/skos/core#> PREFIX sensemaking: <https://sensemaking-ai.com/ns/> SELECT ?skillLabel ?broaderLabel WHERE { :alex sensemaking:hasSkill ?s . ?s skos:prefLabel ?skillLabel ; skos:exactMatch ?escoSkill . ?escoSkill skos:broader ?broader . ?broader skos:prefLabel ?broaderLabel . }
This is the unlock. Not "SPARQL is cleaner" — the SPARQL query has more lines than the Cypher version. The unlock is that the broader-category data exists, is queryable, and did not have to be brought into your database. SKOS is already RDF. ESCO is already published as SKOS. Your skill node lands inside a graph that already knows about programming languages, statistical methods, and the broader occupational shapes a data career takes.
This is where RDF's interop story has teeth.
skos:exactMatch link lands your skill inside ESCO's existing graph. In LPG, ESCO has to be imported as a parallel node set and kept in sync.For each pair of adjacent jobs in chronological order, return the pair. Cypher's path traversal makes this concise.
MATCH (p:Person {name: "Alex Rivera"})-[r1:EMPLOYED_BY]->(c1), (p)-[r2:EMPLOYED_BY]->(c2) WHERE r1.endDate < r2.startDate RETURN c1.name AS prev, c2.name AS next ORDER BY r1.endDate
SPARQL handles it through a self-join on the Employment events, with a FILTER ensuring the second job starts after the first one ends.
SELECT ?prevName ?nextName WHERE { :alex sensemaking:hasEmployment ?e1, ?e2 . ?e1 sensemaking:endDate ?end1 ; sensemaking:employer/schema:name ?prevName . ?e2 sensemaking:startDate ?start2 ; sensemaking:employer/schema:name ?nextName . FILTER(?end1 < ?start2) } ORDER BY ?end1
A wash, but instructively so. Cypher's pattern is slightly easier to write; SPARQL's structure absorbs additional conditions slightly more naturally. Not every query is a clear winner for one stack.
Name the cost. RDF is more verbose than Cypher because every term in it carries a globally identifiable URI. The same fact takes more characters in Turtle. Lines run longer; diffs are noisier; the cognitive overhead of seeing prefixes everywhere does not go away just because you have internalized them.
That cost is real and worth saying out loud. The question is not whether the verbosity exists. It is what the verbosity buys.
For an isolated application — a single graph database serving one team's queries, no external vocabularies, no integration partners — the URI tax is pure overhead. There is no interop story to unlock, no portability to spend. You are paying for something you will not use. LPG is a better fit, and the team will ship faster.
For a graph that participates in a broader ecosystem — interoperating with ESCO, FIBO, schema.org, government open data, anything published as Linked Data — the URI tax is what makes the integration possible at all. The cost is paid up front. The value compounds: every external vocabulary you can read is a query you do not have to engineer separately.
Most practitioners pick the wrong side of this trade-off once before learning to ask the question deliberately. Picking it well the first time is part of what this curriculum is for.
Heuristics, not rules. The kind of starting positions a senior engineer would offer in a design review before reading deeper into requirements.
The honest practitioner has applications where both stacks make sense and chooses based on team skill, operational profile, and integration needs — not on which stack is technically more elegant. Neither is. They are correct for different contexts.
Continue with the Module 1 reading (Allemang Chapters 1–3, if not yet done) and start Exercise 1.3, the Resume Graph Explorer slice. That exercise is the hands-on version of what this page walked through conceptually — you will write the Turtle and the queries yourself on your own data, and the difference between reading the comparison and feeling it in your own resume is most of the learning. Sub-module 1.2 follows when it ships.
The Sensemaking Semantic Web curriculum is a 12-week self-directed program working through the semantic web stack from foundations to a deployed hybrid LLM + knowledge graph system. The full syllabus and Module 1 README live in the curriculum repo. The Module 1 cheat sheet is the back-pocket syntax reference for Turtle and SPARQL.
Five annotated links. Each one earns its place; this is not a dump.