Transactions
In RushDB, Transactions provide a mechanism to group multiple database operations into a single atomic unit of work. They ensure data consistency by guaranteeing that either all operations within the transaction succeed, or none of them do.
How It Works
Transactions in RushDB are built on Neo4j's native transaction capabilities, providing ACID guarantees:
- Atomicity: All operations within a transaction either succeed completely or fail completely, with no partial changes.
- Consistency: Transactions transform the database from one valid state to another, maintaining data integrity.
- Isolation: Concurrent transactions do not interfere with each other, ensuring data consistency.
- Durability: Once a transaction is committed, changes are permanent even in case of system failure.
For example:
- TypeScript
- Python
// Start a transaction
const tx = await db.tx.begin({ ttl: 10000 });
try {
// Create a new user record within the transaction
const user = await db.records.create(
{
label: 'User',
data: {
name: 'Alice Smith',
email: 'alice@example.com',
emailConfirmed: false
}
},
tx
);
// Create a related profile record within the same transaction
const profile = await db.record.create(
{
label: "Profile",
data: {
bio: "Software engineer",
joinDate: new Date().toISOString()
}
},
tx
);
// Create a relationship between the records within the same transaction
await db.records.attach(
{
source: user,
target: profile,
options: {
type: 'HAS_PROFILE'
}
},
tx
);
// Commit the transaction to make all changes permanent
await tx.commit();
// or await db.tx.commit(tx);
} catch (error) {
// If any operation fails, roll back the entire transaction
await tx.rollback();
// or await db.tx.rollback(tx);
throw error;
}
# Start a transaction
tx = db.tx.begin(ttl=10000)
try:
# Create a new user record within the transaction
user = db.records.create(
label="User",
data={
"name": "Alice Smith",
"email": "alice@example.com",
"emailConfirmed": False
},
transaction=tx
)
# Create a related profile record within the same transaction
profile = db.records.create(
label="Profile",
data={
"bio": "Software engineer",
"joinDate": datetime.now().isoformat()
},
transaction=tx
)
# Create a relationship between the records within the same transaction
db.records.attach(
source=user,
target=profile,
options={
"type": "HAS_PROFILE"
},
transaction=tx
)
# Commit the transaction to make all changes permanent
tx.commit()
# or db.tx.commit(tx)
except Exception as error:
# If any operation fails, roll back the entire transaction
tx.rollback()
# or db.tx.rollback(tx)
raise error
Transaction Lifecycle
Each transaction in RushDB follows a clear lifecycle:
- Creation: A transaction is initiated with an optional Time-To-Live (TTL) parameter
- Operation Phase: Multiple database operations are performed using the transaction ID
- Termination: The transaction is explicitly committed to make changes permanent or rolled back to discard all changes
- Automatic Cleanup: If neither committed nor rolled back within the TTL, the transaction is automatically rolled back
Internal Structure
Built on Neo4j's transaction management system, RushDB transactions maintain several internal states:
State | Description |
---|---|
Active | Transaction is open and can accept operations |
Committed | Transaction has been successfully completed |
Rolled Back | Transaction has been explicitly or automatically reverted |
Timed Out | Transaction exceeded its TTL and was automatically rolled back |
Internally, RushDB maintains a transaction registry that:
- Tracks all active transactions
- Monitors their TTL
- Maps transaction IDs to internal Neo4j transaction objects
- Manages transaction cleanup and resource release
For more information about the underlying storage system, see Storage.
Use Cases
Transactions are particularly valuable in several scenarios:
Complex Data Operations
When creating interconnected data structures, transactions ensure that all components are created successfully or not at all:
This approach ensures that complex data structures are properly maintained, with Records and their Relationships remaining consistent.
Concurrent Operations
When multiple users or services access the same data simultaneously, transactions maintain data consistency:
- TypeScript
- Python
// Service 1
const tx1 = await db.tx.begin();
try {
const record = await db.records.findById(recordId, tx1);
await db.records.update(
{
target: record,
label: record.label(),
data: { status: "processing" }
},
tx1
);
await tx1.commit();
} catch (error) {
await tx1.rollback();
}
// Service 2 (concurrent)
const tx2 = await db.tx.begin();
try {
const record = await db.records.findById(recordId, tx2);
// Will see the original record state until tx1 is committed
await tx2.commit()
} catch (error) {
await tx2.rollback()
}
# Service 1
tx1 = db.tx.begin()
try:
record = db.records.find_by_id(record_id, transaction=tx1)
db.records.update(
target=record,
label=record.label(),
data={"status": "processing"},
transaction=tx1
)
tx1.commit()
except Exception as error:
tx1.rollback()
# Service 2 (concurrent)
tx2 = db.tx.begin()
try:
record = db.records.find_by_id(record_id, transaction=tx2)
# Will see the original record state until tx1 is committed
tx2.commit()
except Exception as error:
tx2.rollback()
Transactions help prevent race conditions when multiple operations might affect the same Records or Properties.
Data Migrations
When upgrading data structures or transforming records, transactions ensure that data integrity is maintained:
- TypeScript
- Python
const tx = await db.tx.begin({ ttl: 30000 }); // Longer TTL for migrations
try {
const users = await db.records.find({ labels: ["User"] }, tx);
for (const user of users) {
// Create new format record
await db.records.create(
{
label: "Person",
data: {
fullName: `${user.firstName} ${user.lastName}`,
email: user.email,
migratedFrom: user.__id
}
},
tx
);
}
await tx.commit();
} catch (error) {
await tx.rollback();
console.error("Migration failed:", error);
}
tx = db.tx.begin(ttl=30000) # Longer TTL for migrations
try:
users = db.records.find({"labels": ["User"]}, transaction=tx)
for user in users:
# Create new format record
db.records.create(
label="Person",
data={
"fullName": f"{user.get('firstName')} {user.get('lastName')}",
"email": user.get('email'),
"migratedFrom": user.get('__id')
},
transaction=tx
)
tx.commit()
except Exception as error:
tx.rollback()
print(f"Migration failed: {error}")
During migrations, transactions ensure that Labels and Properties are consistently updated across related records.
Time-To-Live (TTL)
RushDB transactions include a configurable TTL mechanism to prevent hanging transactions:
- Default: 5000ms (5 seconds)
- Maximum: 30000ms (30 seconds)
- Purpose: Automatically rolls back transactions that aren't explicitly committed or rolled back within the specified time
- Recommendation: Set TTL based on expected operation duration plus a reasonable buffer
Transaction Limitations
While transactions provide powerful data consistency guarantees, they come with certain limitations:
- Resource Consumption: Active transactions consume database resources, particularly memory
- Performance Impact: Very long-running transactions can impact overall database performance
- TTL Constraints: Maximum TTL is capped at 30 seconds to prevent resource exhaustion
- Isolation Level: RushDB uses Neo4j's default read-committed isolation level
See Storage for more details on how database resources are managed.
Best Practices
To effectively use transactions in RushDB:
- Keep transactions short: Minimize the number and duration of operations within a transaction
- Set appropriate TTL: Choose a TTL that provides enough time for operations to complete without being unnecessarily long
- Explicit termination: Always explicitly commit or rollback transactions rather than relying on automatic TTL-based rollback
- Error handling: Implement proper error handling with rollback in catch blocks
- Avoid nested transactions: Instead of nesting transactions, design workflows to use a single transaction level
- Batch operations: For bulk operations, consider batching changes into multiple smaller transactions
When working with complex data models, refer to Records and Relationships documentation to understand how transactions affect your data structures.
Integration with Neo4j
RushDB's transaction system leverages Neo4j's native transaction capabilities while adding:
- Client-friendly transaction IDs
- Configurable TTL with automatic cleanup
- Cross-platform SDK integration
- HTTP API support via transaction headers
This provides the robustness of Neo4j's proven transaction system with the ease of use of RushDB's modern API design.
For detailed information on using transactions, see REST API - Transactions or through the language-specific SDKs: