Chapter 02 · Property Graph Model: Nodes, Relationships, Labels, Properties, Types, and Schema Discipline
Nodes and Relationships as First-Class Entities: Direction, Identity, and Traversal Semantics
Make graph structure precise: stored direction, relationship type, durable domain identity, and measurable traversal/cardinality behavior.
Learning outcomes
AtlasMart's Chapter 01 graph already contains customers, devices, orders, and products. The next modeling question is whether connected facts behave the way developers think they do. If a supplier supplies a product, an order contains a product, and a shipment fulfills an order, the direction and identity of those relationships change which traversals are meaningful, which duplicates are bugs, and which business keys survive export, restore, and migration.
Distinguish node identity, business identity, element identity, relationship identity, direction, type, and traversal direction.
Explain why every stored Neo4j relationship has exactly one type and a direction even though Cypher may match it undirected.
Use stable constraint-backed domain keys instead of elementId() as durable AtlasMart identifiers.
Measure direction and degree/cardinality from a small graph rather than infer them from a diagram.
Diagnose modeling mistakes where strings or duplicate relationships hide domain semantics.
Continue Chapter 01's free/local baseline: Neo4j Community
2026.07.1, database neo4j, explicit
CYPHER 25 in version-sensitive examples, local
container atlasmart-neo4j, Bolt
127.0.0.1:7687, HTTP 127.0.0.1:7474,
and disposable password atlasmart-course-2026.
Existing AtlasMart identifiers use Community-supported
uniqueness constraints. This chapter adds
:Category, :Supplier,
:Store, and :Shipment identities and
makes graph-model contracts observable.
The generation environment does not provide a running Docker daemon or Neo4j server, so commands are documentation-checked but not presented as captured output. Community supports the uniqueness constraints used in the mandatory lab. Current property-existence, property-type, and key constraints are Enterprise-only; when those stronger contracts are discussed, the Community path uses validation queries plus application/import checks rather than fabricating successful Community DDL.
Nodes and relationships are first-class graph entities
A Neo4j node is a graph entity with zero or
more labels and zero or more properties. A
relationship connects exactly two nodes, has
exactly one relationship type, always has a stored direction,
and may also have properties. That means
(Customer)-[:PLACED]->(Order) is not shorthand
for an order.customerId property. It is
independently addressable graph structure that Cypher can bind,
filter, return, count, and traverse.
Direction should express domain meaning. AtlasMart reads
(Customer)-[:PLACED]->(Order) naturally as
“customer placed order.” Reversing the arrow in storage would
not make the database incorrect, but it would make query
conventions and mental models harder to maintain. At query time,
MATCH (c)-[:PLACED]-(o) deliberately ignores
direction and can match either orientation. That query syntax
does not make stored relationships directionless.
| Concept | What Neo4j guarantees/represents | Modeling consequence |
|---|---|---|
| Node | A structural graph entity with labels/properties | Use for things/events that need independent identity, connections, or lifecycle. |
| Relationship | Exactly two endpoints, one stored direction, exactly one relationship type, optional properties | Use when the connection itself matters to traversal or carries facts. |
| Label | Classification attached to a node | Useful for matching/index/constraint scope; not itself uniqueness. |
| Relationship type | Classification of one relationship |
Should carry domain meaning such as PLACED,
not generic RELATED_TO by default.
|
| elementId() | Identifier with transaction/DBMS-scoped guarantees; mapping is not durable across transactions/environments | Expose only when needed for implementation evidence; use application-generated IDs for business identity. |
Observe direction instead of trusting a picture
CYPHER 25CREATE CONSTRAINT category_id IF NOT EXISTS FOR (c:Category) REQUIRE c.categoryId IS UNIQUE;CREATE CONSTRAINT supplier_id IF NOT EXISTS FOR (s:Supplier) REQUIRE s.supplierId IS UNIQUE;MERGE (cat:Category {categoryId:'CAT-CAMERAS'}) SET cat.name='Cameras'MERGE (s:Supplier {supplierId:'SUP-1001'}) SET s.name='Northwind Optics'MERGE (p:Product {productId:'P-1001'})MERGE (p)-[:IN_CATEGORY]->(cat)MERGE (p)-[:SUPPLIED_BY {preferred:true}]->(s);
The relationship SUPPLIED_BY is stored from product
to supplier because the dominant AtlasMart questions start at a
product. That is a model convention, not a database requirement.
The following query proves orientation from the stored
relationship itself:
CYPHER 25MATCH (p:Product {productId:'P-1001'})-[r:SUPPLIED_BY]->(s:Supplier)RETURN p.productId AS fromProduct, type(r) AS relationshipType, s.supplierId AS toSupplier, r.preferred AS preferred, elementId(r) AS implementationElementId;
elementId(r) is useful evidence that the
relationship is a first-class element, but current Neo4j
documentation explicitly warns that applications should not
treat the returned value as a durable cross-transaction business
key. If a relationship needs durable business identity—for
example, a contractual supply agreement—model an
application-generated agreement ID or reify the agreement as a
node when its lifecycle warrants it.
Directed match versus undirected match
CYPHER 25MATCH (:Product {productId:'P-1001'})-[:SUPPLIED_BY]->(s:Supplier)RETURN s.supplierId AS directedSupplier;CYPHER 25MATCH (:Product {productId:'P-1001'})-[:SUPPLIED_BY]-(other)RETURN labels(other) AS otherLabels, other.supplierId AS supplierId;
The first pattern states an AtlasMart semantic expectation: from
product to supplier. The second asks only whether an adjacent
SUPPLIED_BY relationship exists. On this fixture
both can return the supplier, but they express different
contracts. An undirected match is useful when orientation is
intentionally irrelevant; overusing it can hide accidentally
reversed writes.
Storing (Customer)-[:PLACED]->(Order) does not
prevent two customers from pointing at the same order, nor
does it prevent a shipment from being connected to several
orders. Direction describes the edge orientation; cardinality
and business invariants still need validation, constraints
where available, transaction logic, or a different model.
Deliberately wrong model: opaque foreign-key strings
Suppose AtlasMart creates
(o:Order {orderId:'O-5002', customerId:'C-1001'})
but no PLACED relationship. The graph now contains
a string that refers to another entity but no traversable
connection. Every query must manually re-join the property
values, and graph algorithms/paths cannot treat that reference
as an edge.
CYPHER 25MERGE (o:Order {orderId:'O-5002'}) SET o.customerId='C-1001';MATCH (o:Order) WHERE o.customerId IS NOT NULL AND NOT (:Customer {customerId:o.customerId})-[:PLACED]->(o)RETURN o.orderId, o.customerId;CYPHER 25MATCH (o:Order {orderId:'O-5002'}), (c:Customer {customerId:o.customerId})MERGE (c)-[:PLACED]->(o)REMOVE o.customerId;MATCH (c:Customer)-[:PLACED]->(o:Order {orderId:'O-5002'})RETURN c.customerId, o.orderId;
The repair turns the connection into graph structure and removes the redundant foreign-key-like property once it is no longer required. If another system owns the customer ID reference and synchronization requires preserving it, keep it deliberately and document which copy is authoritative rather than calling duplication inherently wrong.
Lab: measure identity, direction, and degree
CYPHER 25 CREATE CONSTRAINT store_id IF NOT EXISTS FOR (s:Store) REQUIRE s.storeId IS UNIQUE;CYPHER 25 CREATE CONSTRAINT shipment_id IF NOT EXISTS FOR (s:Shipment) REQUIRE s.shipmentId IS UNIQUE;CYPHER 25 MERGE (st:Store {storeId:'ST-TEH-01'}) SET st.name='AtlasMart Central';CYPHER 25 MERGE (sh:Shipment {shipmentId:'SH-7001'}) SET sh.status='IN_TRANSIT';CYPHER 25 MATCH (p:Product {productId:'P-1001'}),(st:Store {storeId:'ST-TEH-01'}) MERGE (p)-[r:STOCKED_AT]->(st) SET r.onHand=14;CYPHER 25 MATCH (o:Order {orderId:'O-5001'}),(sh:Shipment {shipmentId:'SH-7001'}) MERGE (o)-[:FULFILLED_BY]->(sh);
CYPHER 25MATCH (c:Customer)-[:PLACED]->(o:Order)RETURN c.customerId, count(o) AS ordersPlaced ORDER BY c.customerId;CYPHER 25MATCH (p:Product)-[r:STOCKED_AT]->(st:Store)RETURN p.productId, st.storeId, r.onHand ORDER BY p.productId, st.storeId;CYPHER 25MATCH (n)RETURN labels(n) AS labels, count(*) AS nodes ORDER BY labels;
Verification checklist: domain IDs are unique under named
constraints; every PLACED edge points
customer→order; every STOCKED_AT edge points
product→store; elementId() is never copied into a
business-key property; and expected degree/counts are written
down so later changes can be regression-tested.
Check your understanding
- Why can an undirected MATCH succeed even though every relationship is stored with direction?
- Why is elementId() useful for evidence but poor as an AtlasMart business key?
- What semantic information is lost when a customer reference is stored only as an order.customerId string?
- Does relationship direction enforce one customer per order?
- When might a relationship deserve its own domain identifier or intermediate node?
Review the answers
1. Cypher can deliberately ignore orientation while matching; this does not alter the stored direction.
2. Its mapping to elements has limited guarantees outside a transaction and internal identifiers can be reused; application-generated IDs are the durable identity surface.
3. The connection is no longer first-class graph structure, so traversal/path operations cannot follow it directly and integrity becomes application-side reference logic.
4. No. Direction is orientation, not cardinality or uniqueness.
5. When the connection has its own lifecycle, independent references, many properties, participants beyond two endpoints, or temporal/history requirements that make it a first-class business fact.
Summary and next step
Nodes and relationships are structural graph elements, but durable business identity remains an application/model contract. Stored direction is real even when a query matches an edge undirected. Chapter 02 now has stable category, supplier, store, and shipment identities plus explicit relationship semantics that later Cypher can rely on.
Next, inspect what may legally be stored as a property, how
lists/maps differ, and why Neo4j's null represents
missing/unknown state rather than a stored null property.
Authoritative references
- Current Neo4j versions — Official current-release and 5.26 LTS patch snapshot.
- Cypher Manual — Current Cypher language reference.
- Property, structural, and constructed values — Current property/storage type rules, including lists and maps.
- Working with null — Current missing-property and three-valued null semantics.
- Create constraints — Constraint syntax, integrity semantics, backing indexes, and edition boundaries.
- Scalar functions: elementId() — Current elementId() guarantees and warning against durable application identity.
- MATCH — Directed and undirected relationship-pattern matching semantics.
- CREATE — Node and relationship creation semantics.
- Naming rules and recommendations — Current naming recommendations and case sensitivity.