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

CREATE Nodes and Relationships Safely and Understand Transactional Visibility

Create graph entities only when creation is truly new: make transaction boundaries, endpoint identity, visibility, rollback, and retry risk observable before relying on write success.

Intermediate120–145 minutesTransactional CREATE labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

Learning outcomes

AtlasMart's checkout service must create an order and connect it to an existing customer and product. The graph operation is not merely “run CREATE”: the team must know exactly what is visible before commit, which statements are atomic together, and what happens if the client rolls back or loses the connection.

01

Explain CREATE as unconditional entity creation inside a transaction, not an upsert or uniqueness check.

02

Distinguish implicit/autocommit, explicit, and driver-managed transaction boundaries.

03

Predict committed versus uncommitted visibility under Neo4j's default read-committed isolation.

04

Create relationships only after binding the intended endpoint nodes by stable domain identifiers.

05

Use rollback and before/after counts to prove atomic state change rather than inferring success from client-side code flow.

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. Every graph write lives inside a transaction

A transaction is the unit in which Neo4j applies or rejects graph changes atomically. All graph, index, and schema access happens in transactions. Neo4j's default isolation is read committed: another transaction does not observe your uncommitted updates, and write locks are held until the transaction completes. If a transaction fails or is rolled back, its changes do not become committed database state.

Mechanism What it means for AtlasMart What it does not guarantee
Atomicity Order node and required relationships can commit or roll back as one unit. It does not make an unsafe retry idempotent.
Read committed Other transactions do not see uncommitted changes. It does not guarantee repeatable reads for every traversal.
Automatic write locks Conflicting writes are coordinated while the transaction is active. It does not eliminate deadlocks or all lost-update patterns.
Transaction log Committed writes are logged for durability/recovery. It is not a substitute for backup policy.

2. CREATE always creates the pattern you ask for

CREATE is intentionally literal. If a query returns one row and executes CREATE (o:Order ...), a new node is created for that row even if another Order already has the same business values—unless a constraint rejects the write. That makes CREATE correct for facts that are known to be new, but dangerous when a request may be replayed.

Cypher · safe CREATE only after a uniqueness-backed existence decision
CYPHER 25MATCH (c:Customer {customerId:$customerId})MATCH (p:Product {productId:$productId})CREATE (o:Order {  orderId:$orderId,  status:'pending',  createdAt:datetime()})CREATE (c)-[:PLACED]->(o)CREATE (o)-[:CONTAINS {quantity:$quantity, unitPrice:p.price}]->(p)RETURN o.orderId AS orderId;

The order_id uniqueness constraint is the integrity backstop. A second request with the same $orderId fails rather than silently creating a duplicate Order. That is different from making the entire request idempotent; later lessons redesign the write so repeated execution succeeds with the same state.

3. Relationship creation should bind endpoints first

Relationships are first-class entities, but their meaning depends on the correct endpoints. A robust mutation separates node identity lookup from relationship creation. This keeps the PLACED and CONTAINS semantics readable and avoids accidentally creating look-alike endpoint nodes inside a large pattern.

Cypher · bind endpoints, then create one relationship
CYPHER 25MATCH (c:Customer {customerId:'C-1001'})MATCH (o:Order {orderId:'O-WRITE-2001'})CREATE (c)-[r:RELATED_TO {reason:'demo-only'}]->(o)RETURN type(r), c.customerId, o.orderId;

This example is deliberately temporary. It demonstrates relationship creation, not a recommended AtlasMart domain relationship. Delete only this exact demo relationship afterward:

Cypher · remove only the temporary relationship
CYPHER 25MATCH (:Customer {customerId:'C-1001'})-[r:RELATED_TO {reason:'demo-only'}]->(:Order {orderId:'O-WRITE-2001'})DELETE r;

4. Observe rollback instead of trusting console messages

Cypher Shell supports explicit transaction controls :begin, :commit, and :rollback. Use them only in an interactive disposable lab. The acceptance test is the committed graph state after rollback—not whether the CREATE statement reported “Added 1 node” while the transaction was still open.

Cypher Shell · reversible explicit transaction exercise
:beginCYPHER 25CREATE (:Order {orderId:'O-WRITE-ROLLBACK', status:'test'});MATCH (o:Order {orderId:'O-WRITE-ROLLBACK'}) RETURN o.orderId;:rollbackMATCH (o:Order {orderId:'O-WRITE-ROLLBACK'}) RETURN count(o) AS committedCount;

The final committed count must be zero. If you accidentally commit the test, remove only that ID with a targeted cleanup query.

5. Deliberately wrong: CREATE inside a retryable request

Imagine the application sends CREATE (:Order {orderId:$orderId}), the server commits, but the client loses the response and retries. Without a uniqueness-backed business key, the retry can create another logical order. Even with a uniqueness constraint, the retry turns into an exception rather than a clean idempotent success.

The repair is to design a stable request/business identity and use a constraint-backed MERGE or equivalent transactional pattern whose repeated execution converges on the same graph state. If the operation also triggers external side effects—email, payment capture, HTTP calls—those side effects require their own idempotency/outbox discipline because database retry semantics cannot roll back an already-sent external action.

Hands-on lab: prove atomicity and committed state

1. Record the dedicated slice counts. 2. Start an explicit transaction. 3. Create O-WRITE-ROLLBACK and two relationships to bound endpoints. 4. Verify the write inside your transaction. 5. Roll back. 6. Reconnect or run a new autocommit read and prove the order and relationships are absent. 7. Repeat using :commit with a second ID, then remove only that second ID.

Cypher · targeted cleanup after the commit half of the lab
CYPHER 25MATCH (o:Order {orderId:'O-WRITE-COMMIT'})DETACH DELETE o;

Production judgment

Keep write transactions short enough to bound lock duration and memory, but large enough to preserve the invariants that genuinely need atomicity. Measure contention rather than assuming locks are free. In a cluster, route writes through the official driver and use bookmarks/session semantics for causally dependent reads. Do not treat transaction-log durability as backup, and do not hold user interaction, remote API calls, or long sleeps inside a database transaction.

Check your understanding

  1. Why is CREATE not idempotent by itself?
  2. What does read-committed isolation protect here?
  3. Why bind relationship endpoints before CREATE?
  4. What proves rollback worked?
  5. Why are external side effects outside the database a separate idempotency problem?
Review the answers

1. It unconditionally creates the requested pattern for each input row; a replay can create another entity unless a constraint rejects it or the design uses stable match-or-create identity.

2. Other transactions do not observe your uncommitted updates, while your write locks are held until completion; it does not promise repeatable reads for every concurrent traversal.

3. It makes endpoint identity explicit and avoids accidentally creating look-alike nodes as part of a large pattern.

4. A fresh committed-state query showing the dedicated test node and relationships are absent.

5. Database rollback or managed retry cannot unsend an email or undo an already accepted remote payment call.

Summary and next step

CREATE is precise when creation is truly unconditional and the transaction boundary matches the business invariant. The next lesson changes the requirement: repeated requests must converge on one graph state, which is where MERGE, constraints, and concurrency semantics matter.

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.