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.
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.
Explain CREATE as unconditional entity creation
inside a transaction, not an upsert or uniqueness check.
Distinguish implicit/autocommit, explicit, and driver-managed transaction boundaries.
Predict committed versus uncommitted visibility under Neo4j's default read-committed isolation.
Create relationships only after binding the intended endpoint nodes by stable domain identifiers.
Use rollback and before/after counts to prove atomic state change rather than inferring success from client-side code flow.
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.
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 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 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 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 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 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.
: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 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
- Why is CREATE not idempotent by itself?
- What does read-committed isolation protect here?
- Why bind relationship endpoints before CREATE?
- What proves rollback worked?
- 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
- Current Neo4j versions — Release/LTS snapshot used to pin the course baseline.
- Cypher Manual — CREATE — CREATE semantics for nodes, relationships, parameters, and dynamic labels/types.
- Cypher Manual — MERGE — Match-or-create semantics, ON CREATE/ON MATCH, constraints, and concurrent relationship merges.
- Cypher Manual — SET — Property/label updates and map replacement versus mutation.
- Cypher Manual — REMOVE — Property and label removal semantics.
- Cypher Manual — DELETE — DELETE, NODETACH DELETE, DETACH DELETE, and large-delete guidance.
- Operations Manual — transactional behavior — ACID, read-committed isolation, locking, transaction logs, and deadlock behavior.
- Cypher Shell — Interactive transaction commands, parameters, connection behavior, and transaction timeout support.
- Database transactions — Transaction lifecycle, memory, commit and rollback behavior.
- Concurrent data access — Default read-committed isolation, locks, lost updates, and deadlocks.