Chapter 06 · Graph Writes: CREATE, MERGE, SET, REMOVE, DELETE, and Idempotent Mutation

MERGE Match-or-Create Semantics, ON CREATE/ON MATCH, Uniqueness Constraints, and Concurrency Hazards

Use MERGE as precise match-or-create, not magic upsert: constrain stable identity, separate mutable state, test concurrent execution, and make retry behavior safe.

Intermediate → Advanced135–165 minutesMERGE concurrency labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

Learning outcomes

AtlasMart receives the same product catalog update from two workers at nearly the same time. The desired outcome is one Product, one business identity, and predictable ON CREATE/ON MATCH behavior—not two duplicates and not a hidden last-writer surprise.

01

Explain MERGE as exact pattern match-or-create semantics rather than generic SQL UPSERT.

02

Anchor MERGE on stable constraint-backed identity and apply mutable attributes separately.

03

Use ON CREATE SET and ON MATCH SET to distinguish lifecycle events.

04

Explain why MERGE alone does not guarantee node uniqueness under concurrent loads.

05

Test repeated and concurrent writes and interpret constraint violations, locks, and transaction retries.

Chapter 06 baseline · reviewed 9 September 2026

The mandatory lab continues the accepted course baseline: Neo4j Community 2026.07.1, database neo4j, explicit CYPHER 25 on version-sensitive examples, authentication enabled, local Bolt at bolt://localhost:7687, no mandatory APOC/GDS plugin, and the AtlasMart Customer/Product/Order model from Chapters 01–05. Neo4j 5.26.30 remains the current LTS comparison line. Optional concurrency examples use Neo4j Python Driver 6.3.x; the mandatory mutation logic itself is plain Community Cypher.

Safety and evidence rule

This chapter changes graph state. Every destructive example targets dedicated IDs prefixed P-WRITE-, O-WRITE-, or O-DELETE-, and every cleanup query repeats those predicates. Never broaden them to MATCH (n) DETACH DELETE n in a database containing unrelated work. This generation environment does not run Neo4j or Docker, so expected results are stated as deterministic invariants, not fabricated captured output.

Re-establish the isolated Chapter 06 write fixture

The fixture deliberately reuses AtlasMart's stable business identifiers and Community-supported uniqueness constraints. It creates or normalizes one customer, one dedicated product, one dedicated order, and their PLACED/CONTAINS relationships. Because every identity is deterministic and the write pattern uses MERGE, rerunning the fixture should leave the target counts unchanged.

Cypher · idempotent Chapter 06 fixture
CYPHER 25CREATE CONSTRAINT customer_id IF NOT EXISTS FOR (c:Customer) REQUIRE c.customerId IS UNIQUE;CREATE CONSTRAINT product_id IF NOT EXISTS FOR (p:Product) REQUIRE p.productId IS UNIQUE;CREATE CONSTRAINT order_id IF NOT EXISTS FOR (o:Order) REQUIRE o.orderId IS UNIQUE;MERGE (c:Customer {customerId:'C-1001'})  ON CREATE SET c.name='Ava Chen', c.region='eu', c.createdAt=datetime('2026-09-09T00:00:00Z')  ON MATCH  SET c.lastSeenAt=datetime('2026-09-09T00:00:00Z');MERGE (p:Product {productId:'P-WRITE-1001'})  ON CREATE SET p.name='Atlas Travel Camera', p.price=149.90, p.status='active', p.createdAt=datetime('2026-09-09T00:00:00Z')  ON MATCH  SET p.name='Atlas Travel Camera', p.price=149.90, p.status='active';MERGE (o:Order {orderId:'O-WRITE-2001'})  ON CREATE SET o.status='pending', o.source='chapter06', o.createdAt=datetime('2026-09-09T00:00:00Z')  ON MATCH  SET o.source='chapter06';MERGE (c)-[:PLACED]->(o)MERGE (o)-[line:CONTAINS]->(p)  ON CREATE SET line.quantity=1, line.unitPrice=149.90  ON MATCH  SET line.quantity=1, line.unitPrice=149.90;
Cypher · acceptance counts for only the dedicated slice
CYPHER 25MATCH (p:Product {productId:'P-WRITE-1001'})OPTIONAL MATCH (c:Customer {customerId:'C-1001'})-[placed:PLACED]->(o:Order {orderId:'O-WRITE-2001'})OPTIONAL MATCH (o)-[line:CONTAINS]->(p)RETURN count(DISTINCT p) AS products,       count(DISTINCT o) AS orders,       count(DISTINCT placed) AS placedRelationships,       count(DISTINCT line) AS containsRelationships;

The deterministic target for the dedicated slice is 1 / 1 / 1 / 1. Those counts do not imply the entire database contains only one Product or Order; previous chapters intentionally contain additional AtlasMart data.

1. MERGE is MATCH-or-CREATE for the pattern you wrote

MERGE first looks for the exact pattern. If it finds the pattern, it binds it. If it does not, it creates it. This is narrower than the vague phrase “upsert.” The property map inside the MERGE pattern participates in identity matching; putting mutable attributes there can unintentionally change what counts as the same entity.

Risky · mutable name participates in the MERGE identity
CYPHER 25MERGE (p:Product {productId:$productId, name:$name})SET p.price=$priceRETURN p.productId, p.name;

If productId identifies the product but name changes, the pattern no longer means “find this product by ID.” With a uniqueness constraint on productId, Neo4j can reject the attempted conflicting creation. Without the constraint, a concurrent or sequential model error can create multiple logical products.

Preferred · identity in MERGE, mutable attributes in SET
CYPHER 25MERGE (p:Product {productId:$productId})  ON CREATE SET p.createdAt=datetime()SET p.name=$name,    p.price=$price,    p.status=$status,    p.updatedAt=datetime()RETURN p.productId AS productId;

2. Constraints convert a convention into an integrity contract

A Community property uniqueness constraint prevents two Product nodes carrying the same productId. Neo4j's MERGE documentation explicitly recommends constraints for identifying properties because MERGE alone guarantees pattern existence, not uniqueness under concurrent loads.

Cypher · inspect and establish the identity contract
CYPHER 25CREATE CONSTRAINT product_id IF NOT EXISTSFOR (p:Product) REQUIRE p.productId IS UNIQUE;SHOW CONSTRAINTS;

A uniqueness constraint does not require every Product to have productId; missing-value existence/key enforcement has different edition boundaries. In this Community lab the application/model contract says every AtlasMart Product must have a stable ID, and the verification suite explicitly searches for missing IDs.

Cypher · Community validation for missing business IDs
CYPHER 25MATCH (p:Product)WHERE p.productId IS NULLRETURN count(p) AS productsMissingId;

3. ON CREATE and ON MATCH describe lifecycle-specific mutation

Cypher · creation-only and match-only fields
CYPHER 25MERGE (p:Product {productId:'P-WRITE-1001'})  ON CREATE SET p.createdAt=datetime(), p.importRevision=1  ON MATCH  SET p.lastMatchedAt=datetime(), p.importRevision=coalesce(p.importRevision,0)+1SET p.name='Atlas Travel Camera', p.price=149.90RETURN p.productId, p.importRevision;

This is intentionally not idempotent because importRevision increments on every match. The lesson uses it to prove that “MERGE matched” does not mean “repeated execution has the same effect.” Idempotency is a property of the entire mutation, including the SET expressions.

4. Merge relationships only after binding constrained endpoints

Cypher · bound endpoint relationship MERGE
CYPHER 25MATCH (o:Order {orderId:'O-WRITE-2001'})MATCH (p:Product {productId:'P-WRITE-1001'})MERGE (o)-[line:CONTAINS]->(p)  ON CREATE SET line.createdAt=datetime()SET line.quantity=1, line.unitPrice=p.priceRETURN count(line) AS lineCount;

For a relationship between two previously bound nodes, current Neo4j MERGE behavior acquires exclusive locks on the endpoints when needed and re-checks after locking to prevent duplicate creation by concurrent copies of the same relationship MERGE. That is different from node identity: Cypher has no general “one relationship of this type between these nodes” constraint, so the guarantee here comes from MERGE's locking behavior for this exact relationship pattern.

5. Controlled concurrency test with the official driver

The optional test below uses a single shared Driver object and one session per worker. The transaction function is deliberately side-effect free outside Neo4j because execute_write() may invoke it more than once on retryable failures.

Python · concurrent constraint-backed product upserts
from concurrent.futures import ThreadPoolExecutorfrom neo4j import GraphDatabaseURI = 'bolt://localhost:7687'AUTH = ('neo4j', 'atlasmart-course-password')QUERY = '''CYPHER 25MERGE (p:Product {productId:$productId})  ON CREATE SET p.createdAt=datetime()SET p.name=$name, p.price=$price, p.status='active'RETURN p.productId AS id'''def work(driver, i):    with driver.session(database='neo4j') as session:        return session.execute_write(            lambda tx: tx.run(                QUERY,                productId='P-WRITE-CONCURRENT',                name='Concurrent Camera',                price=199.0            ).single()['id']        )with GraphDatabase.driver(URI, auth=AUTH) as driver:    driver.verify_connectivity()    with ThreadPoolExecutor(max_workers=8) as pool:        results = list(pool.map(lambda i: work(driver, i), range(32)))    print(results)

After the workers finish, the acceptance test is not “all futures returned.” Query committed state:

Cypher · concurrency acceptance check
CYPHER 25MATCH (p:Product {productId:'P-WRITE-CONCURRENT'})RETURN count(p) AS nodes, collect(p.name) AS names;

The invariant is one Product node. Exact lock waits, retries, and timings depend on local scheduling and resources and must be measured rather than invented.

6. Deliberately wrong: MERGE the whole mutable graph pattern

Wrong · mutable properties and new endpoint creation are mixed
CYPHER 25MERGE (c:Customer {customerId:$customerId})-[:PLACED]->      (o:Order {orderId:$orderId, status:$status})-[:CONTAINS]->      (p:Product {productId:$productId, price:$price});

This pattern asks Neo4j to find or create the entire shape as written. A status or price change can make the exact pattern fail to match; missing endpoint constraints can create look-alike entities; and debugging which part caused a create is harder. The repair is staged: MERGE each identity node separately using constrained keys, SET mutable state, MATCH/MERGE relationships between bound nodes, and verify cardinality.

Lab: repeat and race the same upsert

Reset P-WRITE-CONCURRENT with a targeted delete, run the single-thread upsert five times, verify one node, then run the optional 32-call concurrency test and verify one node again. Finally query SHOW CONSTRAINTS and record the constraint name/type that protects the identity.

Cypher · targeted concurrency-lab cleanup
CYPHER 25MATCH (p:Product {productId:'P-WRITE-CONCURRENT'})DETACH DELETE p;

Production judgment

Constraint-backed identity improves correctness and lookup performance but can also concentrate contention on hot keys. Retry behavior must be bounded and observable. Keep transaction functions database-focused and idempotent because drivers can retry them. For multi-entity business invariants, do not assume several independent MERGEs are equivalent to a single business transaction; choose the transaction boundary deliberately and test deadlock/retry behavior under realistic concurrency.

Check your understanding

  1. Why is MERGE not a generic SQL UPSERT?
  2. What does the uniqueness constraint add under concurrency?
  3. Can ON MATCH SET make a MERGE operation non-idempotent?
  4. Why bind relationship endpoints before MERGEing the relationship?
  5. Why must a managed transaction callback avoid non-idempotent external side effects?
Review the answers

1. It matches or creates the exact graph pattern supplied; identity and mutable attributes must be modeled explicitly rather than assumed.

2. It turns productId uniqueness into an enforced database invariant and prevents concurrent duplicate node creation that MERGE alone does not guarantee.

3. Yes. An increment, timestamp mutation, append-like update, or other cumulative effect can change state on every replay.

4. It isolates endpoint identity from relationship existence and makes concurrency/semantics easier to reason about.

5. The driver may execute the callback more than once when retrying transient failures.

Summary and next step

Reliable MERGE starts with constrained identity, not with a giant pattern. Next, mutable attributes get their own semantics: patching, full-map replacement, property removal, label migration, and schema evolution.

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.