Chapter 07 · Graph Data Modeling: Aggregates, Relationships, Hyperedges, Hierarchies, and Temporal Graphs

Hierarchies, Trees, DAGs, Bill-of-Materials, Organizational Graphs, and Cycle Constraints

Model hierarchies as explicit structural contracts and treat DAG acyclicity as an invariant that must survive concurrent writes.

Intermediate → Advanced130–160 minutesHierarchy/DAG cycle labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

Learning outcomes

AtlasMart's bill of materials must be a directed acyclic graph (DAG): assemblies can contain subassemblies, but no component may eventually contain itself. A graph database makes traversing hierarchies natural; it does not automatically make every hierarchy valid.

01

Distinguish trees, DAGs and general cyclic graphs by parent-count and cycle rules.

02

Model bill-of-materials quantities as relationship-owned state.

03

Query ancestors/descendants with explicit traversal bounds.

04

Detect a proposed cycle before creating a hierarchy edge and explain concurrency limits.

05

Validate degree, root/leaf expectations and cycle absence after writes.

Chapter 07 baseline · reviewed 9 September 2026

The mandatory lab continues the accepted course baseline: Neo4j Community 2026.07.1, database neo4j, explicit CYPHER 25 for version-sensitive examples, authentication enabled, no mandatory APOC/GDS plugin, and stable AtlasMart domain identifiers from Chapters 01–06. Neo4j 5.26.30 remains the LTS comparison line. Modeling examples use only Community-compatible graph and uniqueness features.

Evidence and safety note

This generation environment does not run Neo4j or Docker. Commands were checked against current official documentation but were not executed here. Expected outputs are deterministic fixture invariants, not fabricated captures. All destructive/refactoring steps are scoped to Chapter 07 identifiers or labTag='ch07'; never replace them with unconstrained production matches.

Re-establish the Chapter 07 modeling fixture

The lab deliberately creates a small supply-chain slice whose questions require explicit semantics: two suppliers, one product, one store, dated supply agreements, and a component hierarchy. Stable domain IDs remain the durable identity; internal element IDs are not used as business keys.

Cypher · Community-compatible identity constraints
CYPHER 25CREATE CONSTRAINT supplier_id IF NOT EXISTS FOR (s:Supplier) REQUIRE s.supplierId IS UNIQUE;CREATE CONSTRAINT store_id IF NOT EXISTS FOR (s:Store) REQUIRE s.storeId IS UNIQUE;CREATE CONSTRAINT product_id IF NOT EXISTS FOR (p:Product) REQUIRE p.productId IS UNIQUE;CREATE CONSTRAINT category_id IF NOT EXISTS FOR (c:Category) REQUIRE c.categoryId IS UNIQUE;CREATE CONSTRAINT agreement_id IF NOT EXISTS FOR (a:SupplyAgreement) REQUIRE a.agreementId IS UNIQUE;CREATE CONSTRAINT component_id IF NOT EXISTS FOR (c:Component) REQUIRE c.componentId IS UNIQUE;
Cypher · deterministic supply-agreement fixture
CYPHER 25MERGE (sup1:Supplier {supplierId:'S-7001'}) SET sup1.name='Northstar Components', sup1.labTag='ch07'MERGE (sup2:Supplier {supplierId:'S-7002'}) SET sup2.name='BlueRiver Plastics', sup2.labTag='ch07'MERGE (store:Store {storeId:'ST-7001'}) SET store.name='AtlasMart Central', store.labTag='ch07'MERGE (prod:Product {productId:'P-7001'}) SET prod.name='Trail Camera Kit', prod.labTag='ch07'MERGE (cat:Category {categoryId:'CAT-7001'}) SET cat.name='Outdoor Imaging', cat.labTag='ch07'MERGE (prod)-[:IN_CATEGORY]->(cat)MERGE (a1:SupplyAgreement {agreementId:'SA-7001'})SET a1.validFrom=date('2026-01-01'), a1.validTo=date('2026-07-01'),    a1.recordedFrom=datetime('2026-01-02T09:00:00Z'), a1.recordedTo=datetime('9999-12-31T00:00:00Z'),    a1.unitPrice=84.0, a1.currency='USD', a1.labTag='ch07'MERGE (sup1)-[:PARTY_TO]->(a1)MERGE (a1)-[:SUPPLIES]->(prod)MERGE (a1)-[:DELIVERS_TO]->(store)MERGE (a2:SupplyAgreement {agreementId:'SA-7002'})SET a2.validFrom=date('2026-07-01'), a2.validTo=date('2027-01-01'),    a2.recordedFrom=datetime('2026-06-15T10:00:00Z'), a2.recordedTo=datetime('9999-12-31T00:00:00Z'),    a2.unitPrice=79.0, a2.currency='USD', a2.labTag='ch07'MERGE (sup2)-[:PARTY_TO]->(a2)MERGE (a2)-[:SUPPLIES]->(prod)MERGE (a2)-[:DELIVERS_TO]->(store);
Cypher · deterministic bill-of-materials fixture
CYPHER 25MERGE (kit:Component {componentId:'CMP-KIT'}) SET kit.name='Trail Camera Kit', kit.labTag='ch07'MERGE (cam:Component {componentId:'CMP-CAM'}) SET cam.name='Camera Module', cam.labTag='ch07'MERGE (case:Component {componentId:'CMP-CASE'}) SET case.name='Weather Case', case.labTag='ch07'MERGE (lens:Component {componentId:'CMP-LENS'}) SET lens.name='Lens Assembly', lens.labTag='ch07'MERGE (kit)-[:CONTAINS_COMPONENT {quantity:1}]->(cam)MERGE (kit)-[:CONTAINS_COMPONENT {quantity:1}]->(case)MERGE (cam)-[:CONTAINS_COMPONENT {quantity:1}]->(lens);

Use SHOW CONSTRAINTS and targeted counts before refactoring. A successful query proves only the fixture state, not that the model scales to production degree distributions or workload volume.

1. Tree, DAG and general graph are different contracts

Structure Parent rule Cycle rule AtlasMart example
Tree Each non-root node has one parent No cycles Strict category tree
DAG A node may have multiple parents No directed cycles Reusable component/subassembly BOM
General directed graph No parent restriction Cycles may be valid Routing or dependency graph where loops are meaningful

The fixture BOM is a DAG: the lens belongs under the camera module, which belongs under the kit. A reusable subassembly could legitimately have more than one parent, so enforcing “tree” rules would be too strict.

2. Quantities belong to the assembly-component edge

Cypher · query BOM with edge-owned quantity
CYPHER 25MATCH p=(kit:Component {componentId:'CMP-KIT'})-[:CONTAINS_COMPONENT*1..5]->(part:Component)RETURN part.componentId AS component,       length(p) AS depth,       [r IN relationships(p) | r.quantity] AS quantitiesORDER BY depth, component;

The relationship quantity describes how many child components one parent assembly requires. If quantity itself needed independent identity, approvals or history, it could become a fact node; do not add that complexity without a requirement.

3. Detect a cycle before adding an edge

To add parent → child in a DAG, reject the write if child can already reach parent. The following precondition detects that introducing CMP-LENS → CMP-KIT would close a cycle.

Cypher · cycle precondition
CYPHER 25MATCH (parent:Component {componentId:$parentId}),      (child:Component {componentId:$childId})OPTIONAL MATCH path=(child)-[:CONTAINS_COMPONENT*0..20]->(parent)RETURN parent.componentId, child.componentId,       count(path) > 0 AS wouldCreateCycle;

The depth limit is a safety bound, not a proof for arbitrarily deep production DAGs. Production code should know the maximum supported hierarchy depth or use a robust invariant-maintenance strategy.

4. Deliberately wrong: create then discover the cycle

Wrong · creates an invalid edge first
CYPHER 25MATCH (lens:Component {componentId:'CMP-LENS'}),(kit:Component {componentId:'CMP-KIT'})CREATE (lens)-[:CONTAINS_COMPONENT {quantity:1}]->(kit);

Once this edge exists, variable-length traversals can revisit nodes and hierarchy semantics break. Repair by deleting exactly the invalid relationship, then place the reachability check and create operation in one controlled write transaction.

Cypher · remove only the deliberate cycle edge
CYPHER 25MATCH (:Component {componentId:'CMP-LENS'})-[r:CONTAINS_COMPONENT]->(:Component {componentId:'CMP-KIT'})DELETE r;

5. Concurrency is the hard boundary

Two transactions can each pass a reachability precheck against the same committed graph and together create a cycle. Neo4j does not provide a general declarative “this relationship type must remain acyclic” constraint. If concurrent hierarchy edits are possible, the application must coordinate writes—for example through a serialized hierarchy-edit service, appropriate locking strategy, or another invariant-control mechanism—and revalidate after commit.

Hands-on DAG lab

Query the fixture descendants; measure roots/leaves and parent counts; try the deliberate cycle and inspect the changed reachability; repair it; then add a safe second parent to demonstrate a DAG that is not a tree.

Cypher · invariant checks
CYPHER 25MATCH (c:Component) WHERE c.labTag='ch07'OPTIONAL MATCH (parent:Component)-[:CONTAINS_COMPONENT]->(c)WITH c, count(parent) AS parentsRETURN c.componentId, parents ORDER BY c.componentId;MATCH p=(c:Component)-[:CONTAINS_COMPONENT*1..10]->(c)RETURN count(p) AS directedCycles;

Check your understanding

  1. What makes a DAG different from a tree?
  2. Why is quantity on CONTAINS_COMPONENT natural?
  3. How can a proposed edge be checked for a cycle?
  4. Why is a precheck not automatically concurrency-safe?
  5. Does Neo4j provide a general acyclicity constraint?
Review the answers

1. A DAG can give a node multiple parents but still forbids directed cycles.

2. It describes the specific parent-child assembly relation.

3. Before adding parent→child, test whether child already reaches parent.

4. Two concurrent writers can both pass against the old committed graph and jointly violate the invariant.

5. No; DAG acyclicity is normally enforced by application/transaction design and validation.

Summary and next step

Hierarchy semantics are invariants, not drawing conventions. The next lesson adds time: facts can be structurally correct yet historically wrong if the model overwrites previous validity.

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.