Transactions
Learn how to use transactions to ensure data consistency and atomicity when performing multiple graph operations.
Quick Example
// All operations succeed or all fail together
await ductape.graph.executeTransaction(async (tx) => {
// Create user node
const user = await ductape.graph.createNode({
labels: ['User'],
properties: { name: 'Alice', email: 'alice@example.com' },
}, tx);
// Create profile node
const profile = await ductape.graph.createNode({
labels: ['Profile'],
properties: { bio: 'Software Engineer' },
}, tx);
// Link them with a relationship
await ductape.graph.createRelationship({
type: 'HAS_PROFILE',
startNodeId: user.node.id,
endNodeId: profile.node.id,
}, tx);
// If any operation fails, all changes are rolled back
});
Why Use Transactions?
Transactions ensure ACID properties:
- Atomicity: All operations succeed or all fail - no partial updates
- Consistency: Database moves from one valid state to another
- Isolation: Concurrent transactions don't interfere
- Durability: Committed changes are permanent
Without Transactions (Dangerous)
// Create user
const user = await ductape.graph.createNode({
labels: ['User'],
properties: { name: 'Bob', balance: 1000 },
});
// Transfer money - WHAT IF THIS FAILS?
const recipient = await ductape.graph.findNodeById('recipient-id');
// Update user balance
await ductape.graph.updateNode({
id: user.node.id,
properties: { balance: 500 },
});
// ❌ System crashes here - money is lost!
// Update recipient balance (never executed)
await ductape.graph.updateNode({
id: recipient.id,
properties: { balance: recipient.properties.balance + 500 },
});
With Transactions (Safe)
await ductape.graph.executeTransaction(async (tx) => {
// Get user
const user = await ductape.graph.findNodeById('user-id', tx);
// Get recipient
const recipient = await ductape.graph.findNodeById('recipient-id', tx);
// Deduct from user
await ductape.graph.updateNode({
id: user.id,
properties: { balance: user.properties.balance - 500 },
}, tx);
// Add to recipient
await ductape.graph.updateNode({
id: recipient.id,
properties: { balance: recipient.properties.balance + 500 },
}, tx);
// Both updates happen or neither happens
});
Execute Transaction (Recommended)
The easiest way to use transactions - automatically handles commit/rollback:
const result = await ductape.graph.executeTransaction(async (tx) => {
// All operations use the same transaction
const node1 = await ductape.graph.createNode({
labels: ['Product'],
properties: { name: 'Laptop', price: 999 },
}, tx);
const node2 = await ductape.graph.createNode({
labels: ['Category'],
properties: { name: 'Electronics' },
}, tx);
await ductape.graph.createRelationship({
type: 'IN_CATEGORY',
startNodeId: node1.node.id,
endNodeId: node2.node.id,
}, tx);
// Return any value you want
return { productId: node1.node.id, categoryId: node2.node.id };
});
console.log('Created:', result);
// Automatically committed if no errors
// Automatically rolled back if any error occurs
Error Handling
try {
await ductape.graph.executeTransaction(async (tx) => {
const user = await ductape.graph.createNode({
labels: ['User'],
properties: { email: 'alice@example.com' },
}, tx);
// This might fail (e.g., constraint violation)
await ductape.graph.createNode({
labels: ['User'],
properties: { email: 'alice@example.com' }, // Duplicate!
}, tx);
// Transaction automatically rolls back on error
});
} catch (error) {
console.log('Transaction failed, all changes rolled back');
console.error(error.message);
}
Manual Transaction Control
For more control, manage transactions manually:
Basic Pattern
// Begin transaction
const tx = await ductape.graph.beginTransaction();
try {
// Perform operations
const node = await ductape.graph.createNode({
labels: ['User'],
properties: { name: 'Charlie' },
}, tx);
const relationship = await ductape.graph.createRelationship({
type: 'FOLLOWS',
startNodeId: node.node.id,
endNodeId: 'another-node-id',
}, tx);
// Commit transaction
await ductape.graph.commitTransaction(tx);
console.log('Transaction committed successfully');
} catch (error) {
// Rollback on error
await ductape.graph.rollbackTransaction(tx);
console.error('Transaction rolled back:', error.message);
throw error;
}
With Finally Block
const tx = await ductape.graph.beginTransaction();
try {
// Your operations here
await ductape.graph.createNode({
labels: ['Article'],
properties: { title: 'Graph Databases' },
}, tx);
await ductape.graph.commitTransaction(tx);
} catch (error) {
await ductape.graph.rollbackTransaction(tx);
throw error;
} finally {
// Clean up resources if needed
console.log('Transaction completed');
}
Transaction Options
Isolation Levels
await ductape.graph.executeTransaction(
async (tx) => {
// Your operations
},
{
isolation: 'READ_COMMITTED', // or 'SERIALIZABLE'
}
);
Isolation Levels:
READ_COMMITTED: Prevents dirty reads (default)SERIALIZABLE: Highest isolation, prevents all anomalies
Timeout
await ductape.graph.executeTransaction(
async (tx) => {
// Long-running operations
},
{
timeout: 30000, // 30 seconds
}
);
Read-Only Transactions
await ductape.graph.executeTransaction(
async (tx) => {
// Only read operations
const users = await ductape.graph.findNodes({
labels: ['User'],
}, tx);
const stats = await ductape.graph.getStatistics(tx);
return { users, stats };
},
{
readOnly: true, // Optimizes for read performance
}
);
Common Patterns
User Registration
async function registerUser(email: string, name: string, password: string) {
return ductape.graph.executeTransaction(async (tx) => {
// Check if user exists
const existing = await ductape.graph.findNodes({
labels: ['User'],
where: { email },
limit: 1,
}, tx);
if (existing.nodes.length > 0) {
throw new Error('Email already registered');
}
// Create user node
const user = await ductape.graph.createNode({
labels: ['User'],
properties: {
email,
name,
passwordHash: hashPassword(password),
createdAt: new Date(),
},
}, tx);
// Create profile node
const profile = await ductape.graph.createNode({
labels: ['Profile'],
properties: {
userId: user.node.id,
avatar: null,
bio: '',
},
}, tx);
// Link user to profile
await ductape.graph.createRelationship({
type: 'HAS_PROFILE',
startNodeId: user.node.id,
endNodeId: profile.node.id,
}, tx);
// Create default settings
const settings = await ductape.graph.createNode({
labels: ['Settings'],
properties: {
theme: 'light',
notifications: true,
},
}, tx);
// Link user to settings
await ductape.graph.createRelationship({
type: 'HAS_SETTINGS',
startNodeId: user.node.id,
endNodeId: settings.node.id,
}, tx);
return user.node;
});
}
Money Transfer
async function transferMoney(
fromUserId: string,
toUserId: string,
amount: number
) {
return ductape.graph.executeTransaction(async (tx) => {
// Lock and fetch both accounts
const fromUser = await ductape.graph.findNodeById(fromUserId, tx);
const toUser = await ductape.graph.findNodeById(toUserId, tx);
if (!fromUser || !toUser) {
throw new Error('User not found');
}
// Check sufficient balance
if (fromUser.properties.balance < amount) {
throw new Error('Insufficient funds');
}
// Deduct from sender
await ductape.graph.updateNode({
id: fromUserId,
properties: {
balance: fromUser.properties.balance - amount,
},
}, tx);
// Add to recipient
await ductape.graph.updateNode({
id: toUserId,
properties: {
balance: toUser.properties.balance + amount,
},
}, tx);
// Create transaction record
const transaction = await ductape.graph.createNode({
labels: ['Transaction'],
properties: {
amount,
timestamp: new Date(),
type: 'transfer',
},
}, tx);
// Link transaction to users
await ductape.graph.createRelationship({
type: 'SENT',
startNodeId: fromUserId,
endNodeId: transaction.node.id,
}, tx);
await ductape.graph.createRelationship({
type: 'RECEIVED',
startNodeId: toUserId,
endNodeId: transaction.node.id,
}, tx);
return transaction.node;
});
}
Batch Operations
async function createMultipleUsersWithRelationships(
users: Array<{ name: string; email: string }>
) {
return ductape.graph.executeTransaction(async (tx) => {
const createdNodes = [];
// Create all user nodes
for (const userData of users) {
const user = await ductape.graph.createNode({
labels: ['User'],
properties: {
...userData,
createdAt: new Date(),
},
}, tx);
createdNodes.push(user.node);
}
// Create relationships between consecutive users
for (let i = 0; i < createdNodes.length - 1; i++) {
await ductape.graph.createRelationship({
type: 'INVITED_BY',
startNodeId: createdNodes[i + 1].id,
endNodeId: createdNodes[i].id,
properties: {
invitedAt: new Date(),
},
}, tx);
}
return createdNodes;
});
}
Cascade Delete
async function deleteUserAndRelatedData(userId: string) {
return ductape.graph.executeTransaction(async (tx) => {
// Find user
const user = await ductape.graph.findNodeById(userId, tx);
if (!user) {
throw new Error('User not found');
}
// Find all posts by user
const posts = await ductape.graph.findRelationships({
startNodeId: userId,
type: 'POSTED',
direction: 'OUTGOING',
}, tx);
// Delete all posts
for (const post of posts.relationships) {
await ductape.graph.deleteNode({
id: post.endNodeId,
detach: true,
}, tx);
}
// Find and delete profile
const profile = await ductape.graph.findRelationships({
startNodeId: userId,
type: 'HAS_PROFILE',
direction: 'OUTGOING',
}, tx);
if (profile.relationships.length > 0) {
await ductape.graph.deleteNode({
id: profile.relationships[0].endNodeId,
detach: true,
}, tx);
}
// Delete user node
await ductape.graph.deleteNode({
id: userId,
detach: true, // Also deletes all relationships
}, tx);
return { deleted: true, userId };
});
}
Conditional Updates
async function incrementPostLikes(postId: string, userId: string) {
return ductape.graph.executeTransaction(async (tx) => {
// Check if user already liked the post
const existingLike = await ductape.graph.findRelationships({
startNodeId: userId,
endNodeId: postId,
type: 'LIKED',
}, tx);
if (existingLike.relationships.length > 0) {
throw new Error('Already liked this post');
}
// Create like relationship
await ductape.graph.createRelationship({
type: 'LIKED',
startNodeId: userId,
endNodeId: postId,
properties: {
likedAt: new Date(),
},
}, tx);
// Get current post
const post = await ductape.graph.findNodeById(postId, tx);
// Increment likes count
await ductape.graph.updateNode({
id: postId,
properties: {
likes: (post.properties.likes || 0) + 1,
},
}, tx);
return post;
});
}
Transaction Performance
Keep Transactions Short
// ❌ Bad - transaction is open too long
await ductape.graph.executeTransaction(async (tx) => {
const user = await ductape.graph.createNode({ /* ... */ }, tx);
// External API call - blocks transaction
await fetch('https://api.example.com/notify', {
method: 'POST',
body: JSON.stringify(user),
});
await ductape.graph.updateNode({ /* ... */ }, tx);
});
// Good - keep transaction short
const user = await ductape.graph.executeTransaction(async (tx) => {
const user = await ductape.graph.createNode({ /* ... */ }, tx);
await ductape.graph.updateNode({ /* ... */ }, tx);
return user;
});
// External API call after transaction commits
await fetch('https://api.example.com/notify', {
method: 'POST',
body: JSON.stringify(user),
});
Batch Operations
// Batch operations in single transaction
await ductape.graph.executeTransaction(async (tx) => {
const nodes = [];
for (const data of largeDataset) {
const node = await ductape.graph.createNode({
labels: ['Product'],
properties: data,
}, tx);
nodes.push(node);
}
return nodes;
});
Read-Only Optimization
// Use read-only transactions for analytics queries
await ductape.graph.executeTransaction(
async (tx) => {
const stats = await ductape.graph.getStatistics(tx);
const popularPosts = await ductape.graph.findNodes({
labels: ['Post'],
where: { likes: { $GT: 1000 } },
}, tx);
return { stats, popularPosts };
},
{ readOnly: true } // Allows database to optimize
);
Best Practices
1. Always Use Transactions for Multiple Operations
// Good - ensures atomicity
await ductape.graph.executeTransaction(async (tx) => {
const user = await ductape.graph.createNode({ /* ... */ }, tx);
await ductape.graph.createNode({ /* profile */ }, tx);
await ductape.graph.createRelationship({ /* link */ }, tx);
});
2. Use executeTransaction Instead of Manual Control
// Recommended - automatic commit/rollback
await ductape.graph.executeTransaction(async (tx) => {
// operations
});
// ❌ Avoid unless you need fine control
const tx = await ductape.graph.beginTransaction();
try {
// operations
await ductape.graph.commitTransaction(tx);
} catch (error) {
await ductape.graph.rollbackTransaction(tx);
}
3. Handle Errors Appropriately
try {
await ductape.graph.executeTransaction(async (tx) => {
// operations that might fail
});
} catch (error) {
if (error.message.includes('constraint')) {
console.log('Data validation error');
} else if (error.message.includes('deadlock')) {
console.log('Retry transaction');
} else {
console.log('Unexpected error');
}
throw error;
}
4. Don't Nest Transactions
// ❌ Bad - nested transactions not supported
await ductape.graph.executeTransaction(async (tx) => {
await ductape.graph.createNode({ /* ... */ }, tx);
// Don't start another transaction here
await ductape.graph.executeTransaction(async (tx2) => {
// This won't work as expected
});
});
// Good - single transaction for all operations
await ductape.graph.executeTransaction(async (tx) => {
await ductape.graph.createNode({ /* ... */ }, tx);
await ductape.graph.createNode({ /* ... */ }, tx);
await ductape.graph.createRelationship({ /* ... */ }, tx);
});
5. Pass Transaction to All Operations
// Correct - all operations in same transaction
await ductape.graph.executeTransaction(async (tx) => {
const user = await ductape.graph.createNode({ /* ... */ }, tx);
const profile = await ductape.graph.createNode({ /* ... */ }, tx);
await ductape.graph.createRelationship({ /* ... */ }, tx);
});
// ❌ Wrong - missing tx parameter means not in transaction
await ductape.graph.executeTransaction(async (tx) => {
const user = await ductape.graph.createNode({ /* ... */ }, tx);
const profile = await ductape.graph.createNode({ /* ... */ }); // Missing tx!
await ductape.graph.createRelationship({ /* ... */ }, tx);
});
6. Retry on Deadlocks
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (error.message.includes('deadlock') && i < maxRetries - 1) {
console.log(`Deadlock detected, retry ${i + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, 100 * (i + 1)));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
// Usage
await withRetry(() =>
ductape.graph.executeTransaction(async (tx) => {
// operations that might deadlock
})
);
Database-Specific Behavior
Neo4j
- Supports full ACID transactions
- Deadlock detection and prevention
- Optimistic locking
AWS Neptune
- Supports transactions via Gremlin and openCypher
- Eventual consistency for read replicas
ArangoDB
- Multi-document transactions supported
- ACID across collections
Memgraph
- Full transaction support
- ACID compliance
Next Steps
- Indexes & Constraints - Optimize performance and data integrity
- Best Practices - Graph database optimization
- Nodes - Working with graph nodes
- Relationships - Managing connections
See Also
- Graph Overview - Full API reference
- Error Handling - Managing errors effectively