Chapter 02 · Property Graph Model: Nodes, Relationships, Labels, Properties, Types, and Schema Discipline

Schema-Optional Does Not Mean Model-Free: Contracts, Naming, Cardinality, and Invariants

Schema-optional is not model-free: make vocabulary, identity, cardinality, required shape, and evolution rules executable and testable.

Intermediate110–130 minutesSchema discipline and invariant labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

Learning outcomes

A schema-optional database can accept many shapes, which is useful during evolution but dangerous when teams interpret “optional” as “anything goes.” AtlasMart needs a contract describing which labels/types exist, which domain IDs are unique, which relationships are allowed, which properties are required by business policy, and which degree/cardinality boundaries are monitored.

01

Separate physical schema flexibility from explicit domain-model contracts.

02

Adopt consistent naming for labels, relationship types, properties, and named constraints.

03

Measure cardinality/degree and identify one-to-one, one-to-many, and many-to-many expectations that Neo4j does not automatically enforce.

04

Use Community uniqueness constraints plus validation queries for integrity that requires Enterprise existence/type/key constraints.

05

Design safe model evolution with additive changes, backfill, validation, cutover, and rollback.

Chapter 02 continuity contract

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.

Execution and edition note

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.

Schema-optional means enforcement is selective, not absent

Neo4j does not require every :Product node to have the same property set merely because they share a label. That flexibility is different from having no model. AtlasMart still needs an agreed vocabulary and invariants: :Customer(customerId), :Product(productId), :Order(orderId), :Category(categoryId), and relationships such as PLACED, CONTAINS, IN_CATEGORY, SUPPLIED_BY, and STOCKED_AT.

Current Neo4j naming recommendations use PascalCase labels, SCREAMING_SNAKE_CASE relationship types, and camelCase property names. Consistency matters because these names are case sensitive. :Product and :product are different labels; productId and ProductId are different property keys.

Contract layer Example Enforcement in mandatory Community lab
Vocabulary :Product, :Supplier, SUPPLIED_BY Team/model review + tests
Business identity Product.productId unique Named uniqueness constraint
Required property Every product must have name Validation query + import/application test; existence constraint is Enterprise-only
Property type price numeric Validation query + application contract; type constraint is Enterprise-only
Cardinality One order should be placed by exactly one customer Degree/cardinality validation query + transaction/application policy
Relationship endpoint contract SUPPLIED_BY should connect Product→Supplier Pattern validation query

Freeze a model contract as executable evidence

Cypher · named Community uniqueness constraints
CYPHER 25 CREATE CONSTRAINT customer_id IF NOT EXISTS FOR (c:Customer) REQUIRE c.customerId IS UNIQUE;CYPHER 25 CREATE CONSTRAINT product_id IF NOT EXISTS FOR (p:Product) REQUIRE p.productId IS UNIQUE;CYPHER 25 CREATE CONSTRAINT order_id IF NOT EXISTS FOR (o:Order) REQUIRE o.orderId IS UNIQUE;CYPHER 25 CREATE CONSTRAINT category_id IF NOT EXISTS FOR (c:Category) REQUIRE c.categoryId IS UNIQUE;CYPHER 25 CREATE CONSTRAINT supplier_id IF NOT EXISTS FOR (s:Supplier) REQUIRE s.supplierId IS UNIQUE;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;

Uniqueness prevents duplicate non-null values; it does not require the property to exist on every node. That distinction is important. An Enterprise node key combines existence and uniqueness, and Enterprise property-existence/type constraints can enforce additional contracts, but this course does not use those as the mandatory path.

Cypher · Community validation for required IDs/names and types
CYPHER 25MATCH (p:Product)WHERE p.productId IS NULL OR p.name IS NULLRETURN p, 'missing required product identity/name' AS violation;CYPHER 25MATCH (p:Product)WHERE p.price IS NOT NULL AND NOT (p.price IS :: INTEGER | FLOAT)RETURN p.productId, p.price, valueType(p.price) AS actualType;CYPHER 25MATCH (o:Order)OPTIONAL MATCH (c:Customer)-[:PLACED]->(o)WITH o, count(c) AS placingCustomersWHERE placingCustomers <> 1RETURN o.orderId, placingCustomers;

These validation queries are executable model documentation. They can run in CI against fixtures, after imports, and during migrations. They do not provide the same concurrency-time enforcement as database constraints, so the course labels that difference rather than pretending validation is equivalent.

Cardinality lives in the data unless you enforce it

Graph diagrams often draw one arrow and imply “one.” The database stores however many matching relationships the application creates. AtlasMart expects each order to have one placing customer, products may have several suppliers, products may be stocked at many stores, and categories may contain many products. These are business cardinalities, not automatic consequences of the property-graph model.

Cypher · measure current degree distributions
CYPHER 25MATCH (o:Order)OPTIONAL MATCH (c:Customer)-[:PLACED]->(o)RETURN o.orderId, count(c) AS inboundPLACEDORDER BY inboundPLACED DESC, o.orderId;CYPHER 25MATCH (p:Product)OPTIONAL MATCH (p)-[:SUPPLIED_BY]->(s:Supplier)RETURN p.productId, count(s) AS suppliersORDER BY suppliers DESC, p.productId;CYPHER 25MATCH (p:Product)OPTIONAL MATCH (p)-[:STOCKED_AT]->(s:Store)RETURN p.productId, count(s) AS storesORDER BY stores DESC, p.productId;

Beyond correctness, degree affects traversal fan-out and performance. A :Product with ten supplier edges behaves differently from a hub with ten million edges. Chapter 26 will profile those costs; Chapter 02 records the expected cardinality distribution so “high degree” is a measurable model property, not a surprise incident symptom.

Deliberately break an invariant: two customers place one order

Cypher · isolated bad relationship
CYPHER 25MATCH (o:Order {orderId:'O-5001'}), (c:Customer {customerId:'C-1002'})MERGE (c)-[:PLACED]->(o);CYPHER 25MATCH (o:Order)OPTIONAL MATCH (c:Customer)-[:PLACED]->(o)WITH o, collect(c.customerId) AS customerIdsWHERE size(customerIds) <> 1RETURN o.orderId, customerIds;

Neo4j accepts the second relationship because no current Community constraint states “exactly one inbound PLACED edge per order.” The repair is to decide the invariant owner. For this course, the write transaction/application policy must match the order by orderId, reject an existing different placer, and only then create the relationship. The validation query remains the regression check.

Cypher · repair the specific fixture
CYPHER 25MATCH (:Customer {customerId:'C-1002'})-[r:PLACED]->(:Order {orderId:'O-5001'})DELETE r;MATCH (o:Order {orderId:'O-5001'})OPTIONAL MATCH (c:Customer)-[:PLACED]->(o)RETURN o.orderId, collect(c.customerId) AS customerIds;
Do not generalize the repair into production deletion.

The command deletes only the known synthetic relationship. A production repair must prove which relationship is authoritative, preserve audit requirements, and define rollback/reconciliation before mutating connected data.

Evolve the model with an explicit migration contract

Chapter 01 stored Product.category='Cameras' as a scalar. Chapter 02 decides Category deserves independent identity and traversal. A safe evolution keeps old and new representations temporarily: create :Category nodes, backfill IN_CATEGORY, compare coverage, update readers/writers, and only then remove the old property if it is no longer part of the contract.

Cypher · backfill Category nodes and relationships
CYPHER 25MATCH (p:Product)WHERE p.category IS NOT NULLMERGE (c:Category {categoryId:'CAT-' + toUpper(replace(p.category,' ','-'))})ON CREATE SET c.name=p.categoryMERGE (p)-[:IN_CATEGORY]->(c);CYPHER 25MATCH (p:Product)WHERE p.category IS NOT NULLOPTIONAL MATCH (p)-[:IN_CATEGORY]->(c:Category)WITH p, count(c) AS mappedWHERE mapped = 0RETURN p.productId, p.category;

Only after readers and writers have moved and validation returns no unexplained gaps should AtlasMart consider REMOVE p.category. Keeping a rollback window is a data-model concern, not just a deployment concern.

Lab: produce a schema-discipline report

Cypher · constraint inventory and model-health checks
SHOW CONSTRAINTS YIELD name, type, entityType, labelsOrTypes, propertiesRETURN name, type, entityType, labelsOrTypes, properties ORDER BY name;CYPHER 25MATCH (n)RETURN labels(n) AS labels, count(*) AS nodes ORDER BY labels;CYPHER 25MATCH ()-[r]->()RETURN type(r) AS relationshipType, count(*) AS relationships ORDER BY relationshipType;CYPHER 25MATCH (p:Product)WHERE p.productId IS NULL OR p.name IS NULLRETURN p.productId, p.name;

Acceptance criteria: every domain label has a documented stable key; all current keys are protected by named uniqueness constraints in Community; naming conventions are consistent; relationship endpoint/direction rules are documented; cardinality validation queries return only known exceptions; model migrations specify dual representation, backfill, validation, cutover, and rollback.

Check your understanding

  1. Why does a uniqueness constraint not prove every Product has productId?
  2. What is the difference between model cardinality and relationship direction?
  3. Why are validation queries not fully equivalent to database constraints?
  4. How should AtlasMart migrate category from a scalar property to a node relationship safely?
  5. Why record degree distributions now?
Review the answers

1. Uniqueness applies to values that are present; missing values are not forced to exist by a uniqueness constraint.

2. Direction states edge orientation; cardinality states how many such edges/entities are allowed or expected.

3. Validation detects violations when run but may not prevent concurrent invalid writes at commit time.

4. Create category identities and IN_CATEGORY edges while the old property remains, verify coverage and reader/writer cutover, then remove the old property only after a rollback-aware migration decision.

5. Degree controls correctness expectations and future traversal fan-out/performance, so a baseline makes later anomalies measurable.

Summary and next step

Neo4j's flexible schema becomes production-safe only when vocabulary, identity, property shape, cardinality, relationship endpoints, and migrations are explicit. Community uniqueness constraints plus deterministic validation give this free lab a real integrity contract while preserving the edition boundary. Next, choose durable identifiers and decide when facts belong on relationships versus nodes, especially when time changes their meaning.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.