Transactions
Transactions ensure that multiple database operations execute atomically - either all succeed or all fail. This guide covers transaction basics, savepoints, isolation levels, and best practices.
Quick Example
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({
database: 'main-db',
}, async (transaction) => {
// Create order
const order = await ductape.database.insert({
table: 'orders',
data: { customer_id: 123, total: 99.99, status: 'pending' },
transaction,
});
// Create order items
await ductape.database.insert({
table: 'order_items',
data: items.map(item => ({ order_id: order.insertedIds[0], ...item })),
transaction,
});
// Update inventory
await ductape.database.update({
table: 'products',
data: { stock: { $dec: 1 } },
where: { id: productId },
transaction,
});
return order;
});
// All changes commit on success, or rollback on any error
ductape.database.transaction(Map.of(
"database", "main-db"
), async (transaction) => Map.of(
// Create order
Map<String, Object> order = ductape.database.insert(Map.of(
"table", "orders",
data: Map.of( "customer_id", 123, "total", 99.99, "status", "pending" ),
transaction
));
// Create order items
ductape.database.insert(Map.of(
"table", "order_items",
data: items.map(item => (Map.of( order_id: order.insertedIds[0], ...item ))),
transaction
));
// Update inventory
ductape.database.update(Map.of(
"table", "products",
data: Map.of( stock: Map.of( $"dec", 1 ) ),
where: Map.of( id: productId ),
transaction
));
return order;
));
// All changes commit on success, or rollback on any error
client.database.transaction({
"database": "main-db",
}, async (transaction) => {
// Create order
order := client.database.insert({
"table": "orders",
data: { "customer_id": 123, "total": 99.99, "status": "pending" },
transaction,
});
// Create order items
client.database.insert({
"table": "order_items",
data: items.map(item => ({ order_id: order.insertedIds[0], ...item })),
transaction,
});
// Update inventory
client.database.update({
"table": "products",
data: { stock: { $"dec": 1 } },
where: { id: productId },
transaction,
});
return order;
});
// All changes commit on success, or rollback on any error
await ductape.database.transaction({
["database"] = "main-db",
}, async (transaction) => {
// Create order
var order = await ductape.database.insert({
["table"] = "orders",
data: { ["customer_id"] = 123, ["total"] = 99.99, ["status"] = "pending" },
transaction,
});
// Create order items
await ductape.database.insert({
["table"] = "order_items",
data: items.map(item => ({ order_id: order.insertedIds[0], ...item })),
transaction,
});
// Update inventory
await ductape.database.update({
["table"] = "products",
data: { stock: { $["dec"] = 1 } },
where: { id: productId },
transaction,
});
return order;
});
// All changes commit on success, or rollback on any error
Why Use Transactions?
Without transactions, related operations can partially complete, leaving your data in an inconsistent state:
- TypeScript
- Java
- Go
- .NET
// Without transaction - DANGEROUS
await ductape.database.insert({ table: 'orders', data: orderData });
// If this fails, the order exists but has no items!
await ductape.database.insert({ table: 'order_items', data: itemsData });
// If this fails, inventory isn't updated!
await ductape.database.update({ table: 'products', data: stockUpdate });
// Without transaction - DANGEROUS
ductape.database.insert(Map.of( "table", "orders", data: orderData ));
// If this fails, the order exists but has no items!
ductape.database.insert(Map.of( "table", "order_items", data: itemsData ));
// If this fails, inventory isn't updated!
ductape.database.update(Map.of( "table", "products", data: stockUpdate ));
// Without transaction - DANGEROUS
client.database.insert({ "table": "orders", data: orderData });
// If this fails, the order exists but has no items!
client.database.insert({ "table": "order_items", data: itemsData });
// If this fails, inventory isn't updated!
client.database.update({ "table": "products", data: stockUpdate });
// Without transaction - DANGEROUS
await ductape.database.insert({ ["table"] = "orders", data: orderData });
// If this fails, the order exists but has no items!
await ductape.database.insert({ ["table"] = "order_items", data: itemsData });
// If this fails, inventory isn't updated!
await ductape.database.update({ ["table"] = "products", data: stockUpdate });
With transactions, all operations succeed together or fail together:
- TypeScript
- Java
- Go
- .NET
// With transaction - SAFE
await ductape.database.transaction({ ... }, async (transaction) => {
await ductape.database.insert({ table: 'orders', data: orderData, transaction });
await ductape.database.insert({ table: 'order_items', data: itemsData, transaction });
await ductape.database.update({ table: 'products', data: stockUpdate, transaction });
});
// Either all changes are saved, or none are
// With transaction - SAFE
ductape.database.transaction(Map.of( ... ), async (transaction) => Map.of(
ductape.database.insert(Map.of( "table", "orders", data: orderData, transaction ));
ductape.database.insert(Map.of( "table", "order_items", data: itemsData, transaction ));
ductape.database.update(Map.of( "table", "products", data: stockUpdate, transaction ));
));
// Either all changes are saved, or none are
// With transaction - SAFE
client.database.transaction({ ... }, async (transaction) => {
client.database.insert({ "table": "orders", data: orderData, transaction });
client.database.insert({ "table": "order_items", data: itemsData, transaction });
client.database.update({ "table": "products", data: stockUpdate, transaction });
});
// Either all changes are saved, or none are
// With transaction - SAFE
await ductape.database.transaction({ ... }, async (transaction) => {
await ductape.database.insert({ ["table"] = "orders", data: orderData, transaction });
await ductape.database.insert({ ["table"] = "order_items", data: itemsData, transaction });
await ductape.database.update({ ["table"] = "products", data: stockUpdate, transaction });
});
// Either all changes are saved, or none are
Callback API (Recommended)
The callback API automatically handles commit and rollback:
- TypeScript
- Java
- Go
- .NET
const result = await ductape.database.transaction({
database: 'main-db',
}, async (transaction) => {
// Your database operations here
// All use the same transaction context
const user = await ductape.database.insert({
table: 'users',
data: { name: 'John', email: 'john@example.com' },
transaction,
});
await ductape.database.insert({
table: 'profiles',
data: { user_id: user.insertedIds[0], bio: 'Hello!' },
transaction,
});
return user; // Return value is passed through
});
console.log('User created:', result);
Map<String, Object> result = ductape.database.transaction(Map.of(
"database", "main-db"
), async (transaction) => Map.of(
// Your database operations here
// All use the same transaction context
Map<String, Object> user = ductape.database.insert(Map.of(
"table", "users",
data: Map.of( "name", "John", "email", "john@example.com" ),
transaction
));
ductape.database.insert(Map.of(
"table", "profiles",
data: Map.of( user_id: user.insertedIds[0], "bio", "Hello!" ),
transaction
));
return user; // Return value is passed through
));
System.out.println('User created:', result);
result := client.database.transaction({
"database": "main-db",
}, async (transaction) => {
// Your database operations here
// All use the same transaction context
user := client.database.insert({
"table": "users",
data: { "name": "John", "email": "john@example.com" },
transaction,
});
client.database.insert({
"table": "profiles",
data: { user_id: user.insertedIds[0], "bio": "Hello!" },
transaction,
});
return user; // Return value is passed through
});
fmt.Println('User created:', result);
var result = await ductape.database.transaction({
["database"] = "main-db",
}, async (transaction) => {
// Your database operations here
// All use the same transaction context
var user = await ductape.database.insert({
["table"] = "users",
data: { ["name"] = "John", ["email"] = "john@example.com" },
transaction,
});
await ductape.database.insert({
["table"] = "profiles",
data: { user_id: user.insertedIds[0], ["bio"] = "Hello!" },
transaction,
});
return user; // Return value is passed through
});
Console.WriteLine('User created:', result);
How It Works
- Transaction begins automatically
- Your callback executes
- If callback succeeds → Transaction commits
- If callback throws → Transaction rolls back
- Original error is re-thrown
Manual Transaction Control
For more control, manage the transaction lifecycle manually:
- TypeScript
- Java
- Go
- .NET
const transaction = await ductape.database.beginTransaction({
database: 'main-db',
isolationLevel: 'REPEATABLE_READ',
});
try {
await ductape.database.insert({
table: 'accounts',
data: { balance: 1000 },
transaction,
});
await ductape.database.update({
table: 'accounts',
data: { balance: { $dec: 100 } },
where: { id: sourceId },
transaction,
});
await ductape.database.update({
table: 'accounts',
data: { balance: { $INC: 100 } },
where: { id: targetId },
transaction,
});
// Manually commit
await transaction.commit();
console.log('Transfer complete');
} catch (error) {
// Manually rollback
await transaction.rollback();
console.error('Transfer failed:', error);
throw error;
}
Map<String, Object> transaction = ductape.database.beginTransaction(Map.of(
"database", "main-db",
"isolationLevel", "REPEATABLE_READ"
));
try Map.of(
ductape.database.insert(Map.of(
"table", "accounts",
data: Map.of( "balance", 1000 ),
transaction
));
ductape.database.update(Map.of(
"table", "accounts",
data: Map.of( balance: Map.of( $"dec", 100 ) ),
where: Map.of( id: sourceId ),
transaction
));
ductape.database.update(Map.of(
"table", "accounts",
data: Map.of( balance: Map.of( $"INC", 100 ) ),
where: Map.of( id: targetId ),
transaction
));
// Manually commit
transaction.commit();
System.out.println('Transfer complete');
) catch (error) Map.of(
// Manually rollback
transaction.rollback();
console.error('Transfer failed:', error);
throw error;
)
transaction := client.database.beginTransaction({
"database": "main-db",
"isolationLevel": "REPEATABLE_READ",
});
try {
client.database.insert({
"table": "accounts",
data: { "balance": 1000 },
transaction,
});
client.database.update({
"table": "accounts",
data: { balance: { $"dec": 100 } },
where: { id: sourceId },
transaction,
});
client.database.update({
"table": "accounts",
data: { balance: { $"INC": 100 } },
where: { id: targetId },
transaction,
});
// Manually commit
transaction.commit();
fmt.Println('Transfer complete');
} catch (error) {
// Manually rollback
transaction.rollback();
console.error('Transfer failed:', error);
throw error;
}
var transaction = await ductape.database.beginTransaction({
["database"] = "main-db",
["isolationLevel"] = "REPEATABLE_READ",
});
try {
await ductape.database.insert({
["table"] = "accounts",
data: { ["balance"] = 1000 },
transaction,
});
await ductape.database.update({
["table"] = "accounts",
data: { balance: { $["dec"] = 100 } },
where: { id: sourceId },
transaction,
});
await ductape.database.update({
["table"] = "accounts",
data: { balance: { $["INC"] = 100 } },
where: { id: targetId },
transaction,
});
// Manually commit
await transaction.commit();
Console.WriteLine('Transfer complete');
} catch (error) {
// Manually rollback
await transaction.rollback();
console.error('Transfer failed:', error);
throw error;
}
Savepoints
Savepoints allow partial rollback within a transaction. If a risky operation fails, you can rollback to the savepoint without losing earlier work.
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({
database: 'main-db',
}, async (transaction) => {
// First operation - will be kept
await ductape.database.insert({
table: 'users',
data: { name: 'User 1' },
transaction,
});
// Create savepoint before risky operation
const savepoint = await transaction.savepoint('before_bulk');
try {
// Risky bulk insert
await ductape.database.insert({
table: 'users',
data: Array.from({ length: 1000 }, (_, i) => ({
name: `User ${i + 2}`,
})),
transaction,
});
// Release savepoint if successful
await savepoint.release();
} catch (error) {
// Rollback only to savepoint - User 1 is preserved
await savepoint.rollback();
console.error('Bulk insert failed, rolled back to savepoint');
}
// Continue with more operations...
await ductape.database.insert({
table: 'audit_log',
data: { action: 'user_import', status: 'partial' },
transaction,
});
});
ductape.database.transaction(Map.of(
"database", "main-db"
), async (transaction) => Map.of(
// First operation - will be kept
ductape.database.insert(Map.of(
"table", "users",
data: Map.of( "name", "User 1" ),
transaction
));
// Create savepoint before risky operation
Map<String, Object> savepoint = transaction.savepoint('before_bulk');
try Map.of(
// Risky bulk insert
ductape.database.insert(Map.of(
"table", "users",
data: Array.from(Map.of( "length", 1000 ), (_, i) => (Map.of(
name: `User $Map.of(i + 2)`
))),
transaction
));
// Release savepoint if successful
savepoint.release();
) catch (error) Map.of(
// Rollback only to savepoint - User 1 is preserved
savepoint.rollback();
console.error('Bulk insert failed, rolled back to savepoint');
)
// Continue with more operations...
ductape.database.insert(Map.of(
"table", "audit_log",
data: Map.of( "action", "user_import", "status", "partial" ),
transaction
));
));
client.database.transaction({
"database": "main-db",
}, async (transaction) => {
// First operation - will be kept
client.database.insert({
"table": "users",
data: { "name": "User 1" },
transaction,
});
// Create savepoint before risky operation
savepoint := transaction.savepoint('before_bulk');
try {
// Risky bulk insert
client.database.insert({
"table": "users",
data: Array.from({ "length": 1000 }, (_, i) => ({
name: `User ${i + 2}`,
})),
transaction,
});
// Release savepoint if successful
savepoint.release();
} catch (error) {
// Rollback only to savepoint - User 1 is preserved
savepoint.rollback();
console.error('Bulk insert failed, rolled back to savepoint');
}
// Continue with more operations...
client.database.insert({
"table": "audit_log",
data: { "action": "user_import", "status": "partial" },
transaction,
});
});
await ductape.database.transaction({
["database"] = "main-db",
}, async (transaction) => {
// First operation - will be kept
await ductape.database.insert({
["table"] = "users",
data: { ["name"] = "User 1" },
transaction,
});
// Create savepoint before risky operation
var savepoint = await transaction.savepoint('before_bulk');
try {
// Risky bulk insert
await ductape.database.insert({
["table"] = "users",
data: Array.from({ ["length"] = 1000 }, (_, i) => ({
name: `User ${i + 2}`,
})),
transaction,
});
// Release savepoint if successful
await savepoint.release();
} catch (error) {
// Rollback only to savepoint - User 1 is preserved
await savepoint.rollback();
console.error('Bulk insert failed, rolled back to savepoint');
}
// Continue with more operations...
await ductape.database.insert({
["table"] = "audit_log",
data: { ["action"] = "user_import", ["status"] = "partial" },
transaction,
});
});
Savepoint Support
| Database | Savepoint Support |
|---|---|
| PostgreSQL | Full support |
| MySQL | Full support |
| MongoDB | Not supported (throws error) |
| DynamoDB | Not supported (throws error) |
Isolation Levels
Control how transactions interact with concurrent operations:
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({
database: 'main-db',
isolationLevel: 'SERIALIZABLE',
}, async (transaction) => {
// Operations here
});
ductape.database.transaction(Map.of(
"database", "main-db",
"isolationLevel", "SERIALIZABLE"
), async (transaction) => Map.of(
// Operations here
));
client.database.transaction({
"database": "main-db",
"isolationLevel": "SERIALIZABLE",
}, async (transaction) => {
// Operations here
});
await ductape.database.transaction({
["database"] = "main-db",
["isolationLevel"] = "SERIALIZABLE",
}, async (transaction) => {
// Operations here
});
Available Levels
| Level | Description | Use Case |
|---|---|---|
READ_UNCOMMITTED | Can see uncommitted changes from other transactions | Rarely used, maximum concurrency |
READ_COMMITTED | Only sees committed changes | Default for most databases |
REPEATABLE_READ | Consistent reads within transaction | Reports, analytics |
SERIALIZABLE | Full isolation, transactions execute as if sequential | Financial operations |
Isolation Level Examples
- TypeScript
- Java
- Go
- .NET
// READ_COMMITTED - Default, good for most operations
await ductape.database.transaction({
...config,
isolationLevel: 'READ_COMMITTED',
}, async (trx) => {
// Standard CRUD operations
});
// REPEATABLE_READ - For consistent report generation
await ductape.database.transaction({
...config,
isolationLevel: 'REPEATABLE_READ',
}, async (trx) => {
// Read operations that must see consistent data
const orders = await ductape.database.query({ table: 'orders', transaction: trx });
const totals = await ductape.database.aggregate({ table: 'orders', operations: {...}, transaction: trx });
});
// SERIALIZABLE - For financial transactions
await ductape.database.transaction({
...config,
isolationLevel: 'SERIALIZABLE',
}, async (trx) => {
// Check balance and transfer atomically
const account = await ductape.database.query({ table: 'accounts', where: { id: 1 }, transaction: trx });
if (account.data[0].balance >= amount) {
await ductape.database.update({ table: 'accounts', data: { balance: { $dec: amount } }, transaction: trx });
}
});
// READ_COMMITTED - Default, good for most operations
ductape.database.transaction(Map.of(
...config,
"isolationLevel", "READ_COMMITTED"
), async (trx) => Map.of(
// Standard CRUD operations
));
// REPEATABLE_READ - For consistent report generation
ductape.database.transaction(Map.of(
...config,
"isolationLevel", "REPEATABLE_READ"
), async (trx) => Map.of(
// Read operations that must see consistent data
Map<String, Object> orders = ductape.database.query(Map.of( "table", "orders", transaction: trx ));
Map<String, Object> totals = ductape.database.aggregate(Map.of( "table", "orders", operations: Map.of(...), transaction: trx ));
));
// SERIALIZABLE - For financial transactions
ductape.database.transaction(Map.of(
...config,
"isolationLevel", "SERIALIZABLE"
), async (trx) => Map.of(
// Check balance and transfer atomically
Map<String, Object> account = ductape.database.query(Map.of( "table", "accounts", where: Map.of( "id", 1 ), transaction: trx ));
if (account.data[0].balance >= amount) Map.of(
ductape.database.update(Map.of( "table", "accounts", data: Map.of( balance: Map.of( $dec: amount ) ), transaction: trx ));
)
));
// READ_COMMITTED - Default, good for most operations
client.database.transaction({
...config,
"isolationLevel": "READ_COMMITTED",
}, async (trx) => {
// Standard CRUD operations
});
// REPEATABLE_READ - For consistent report generation
client.database.transaction({
...config,
"isolationLevel": "REPEATABLE_READ",
}, async (trx) => {
// Read operations that must see consistent data
orders := client.database.query({ "table": "orders", transaction: trx });
totals := client.database.aggregate({ "table": "orders", operations: {...}, transaction: trx });
});
// SERIALIZABLE - For financial transactions
client.database.transaction({
...config,
"isolationLevel": "SERIALIZABLE",
}, async (trx) => {
// Check balance and transfer atomically
account := client.database.query({ "table": "accounts", where: { "id": 1 }, transaction: trx });
if (account.data[0].balance >= amount) {
client.database.update({ "table": "accounts", data: { balance: { $dec: amount } }, transaction: trx });
}
});
// READ_COMMITTED - Default, good for most operations
await ductape.database.transaction({
...config,
["isolationLevel"] = "READ_COMMITTED",
}, async (trx) => {
// Standard CRUD operations
});
// REPEATABLE_READ - For consistent report generation
await ductape.database.transaction({
...config,
["isolationLevel"] = "REPEATABLE_READ",
}, async (trx) => {
// Read operations that must see consistent data
var orders = await ductape.database.query({ ["table"] = "orders", transaction: trx });
var totals = await ductape.database.aggregate({ ["table"] = "orders", operations: {...}, transaction: trx });
});
// SERIALIZABLE - For financial transactions
await ductape.database.transaction({
...config,
["isolationLevel"] = "SERIALIZABLE",
}, async (trx) => {
// Check balance and transfer atomically
var account = await ductape.database.query({ ["table"] = "accounts", where: { ["id"] = 1 }, transaction: trx });
if (account.data[0].balance >= amount) {
await ductape.database.update({ ["table"] = "accounts", data: { balance: { $dec: amount } }, transaction: trx });
}
});
Read-Only Transactions
Mark transactions as read-only for optimization:
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({
database: 'main-db',
readOnly: true, // Optimization hint
}, async (transaction) => {
// Only read operations
const users = await ductape.database.query({
table: 'users',
where: { status: 'active' },
transaction,
});
const stats = await ductape.database.aggregate({
table: 'orders',
operations: {
total: { $SUM: 'amount' },
count: { $COUNT: '*' },
},
transaction,
});
return { users: users.data, stats };
});
ductape.database.transaction(Map.of(
"database", "main-db",
"readOnly", true, // Optimization hint
), async (transaction) => Map.of(
// Only read operations
Map<String, Object> users = ductape.database.query(Map.of(
"table", "users",
where: Map.of( "status", "active" ),
transaction
));
Map<String, Object> stats = ductape.database.aggregate(Map.of(
"table", "orders",
operations: Map.of(
total: Map.of( $"SUM", "amount" ),
count: Map.of( $"COUNT", "*" )
),
transaction
));
return Map.of( users: users.data, stats );
));
client.database.transaction({
"database": "main-db",
"readOnly": true, // Optimization hint
}, async (transaction) => {
// Only read operations
users := client.database.query({
"table": "users",
where: { "status": "active" },
transaction,
});
stats := client.database.aggregate({
"table": "orders",
operations: {
total: { $"SUM": "amount" },
count: { $"COUNT": "*" },
},
transaction,
});
return { users: users.data, stats };
});
await ductape.database.transaction({
["database"] = "main-db",
["readOnly"] = true, // Optimization hint
}, async (transaction) => {
// Only read operations
var users = await ductape.database.query({
["table"] = "users",
where: { ["status"] = "active" },
transaction,
});
var stats = await ductape.database.aggregate({
["table"] = "orders",
operations: {
total: { $["SUM"] = "amount" },
count: { $["COUNT"] = "*" },
},
transaction,
});
return { users: users.data, stats };
});
Transaction Timeout
Set a maximum duration for transactions:
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({
database: 'main-db',
timeout: 30000, // 30 seconds
}, async (transaction) => {
// Operations must complete within 30 seconds
// Otherwise, transaction is rolled back
});
ductape.database.transaction(Map.of(
"database", "main-db",
"timeout", 30000, // 30 seconds
), async (transaction) => Map.of(
// Operations must complete within 30 seconds
// Otherwise, transaction is rolled back
));
client.database.transaction({
"database": "main-db",
"timeout": 30000, // 30 seconds
}, async (transaction) => {
// Operations must complete within 30 seconds
// Otherwise, transaction is rolled back
});
await ductape.database.transaction({
["database"] = "main-db",
["timeout"] = 30000, // 30 seconds
}, async (transaction) => {
// Operations must complete within 30 seconds
// Otherwise, transaction is rolled back
});
Error Handling
Handling Specific Errors
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({
database: 'main-db',
}, async (transaction) => {
try {
await ductape.database.insert({
table: 'users',
data: { email: existingEmail },
transaction,
});
} catch (error) {
if (error.code === 'UNIQUE_VIOLATION') {
// Handle gracefully without killing transaction
console.log('User already exists, skipping...');
// Continue with other operations
} else {
// Rethrow to rollback entire transaction
throw error;
}
}
// More operations...
});
ductape.database.transaction(Map.of(
"database", "main-db"
), async (transaction) => Map.of(
try Map.of(
ductape.database.insert(Map.of(
"table", "users",
data: Map.of( email: existingEmail ),
transaction
));
) catch (error) Map.of(
if (error.code === 'UNIQUE_VIOLATION') Map.of(
// Handle gracefully without killing transaction
System.out.println('User already exists, skipping...');
// Continue with other operations
) else Map.of(
// Rethrow to rollback entire transaction
throw error;
)
)
// More operations...
));
client.database.transaction({
"database": "main-db",
}, async (transaction) => {
try {
client.database.insert({
"table": "users",
data: { email: existingEmail },
transaction,
});
} catch (error) {
if (error.code === 'UNIQUE_VIOLATION') {
// Handle gracefully without killing transaction
fmt.Println('User already exists, skipping...');
// Continue with other operations
} else {
// Rethrow to rollback entire transaction
throw error;
}
}
// More operations...
});
await ductape.database.transaction({
["database"] = "main-db",
}, async (transaction) => {
try {
await ductape.database.insert({
["table"] = "users",
data: { email: existingEmail },
transaction,
});
} catch (error) {
if (error.code === 'UNIQUE_VIOLATION') {
// Handle gracefully without killing transaction
Console.WriteLine('User already exists, skipping...');
// Continue with other operations
} else {
// Rethrow to rollback entire transaction
throw error;
}
}
// More operations...
});
Transaction Status
- TypeScript
- Java
- Go
- .NET
const transaction = await ductape.database.beginTransaction({
database: 'main-db',
});
console.log('Active:', transaction.isActive()); // true
await transaction.commit();
console.log('Active:', transaction.isActive()); // false
console.log('Status:', transaction.status); // 'committed'
Map<String, Object> transaction = ductape.database.beginTransaction(Map.of(
"database", "main-db"
));
System.out.println('"Active", ", transaction.isActive()); // true
transaction.commit();
System.out.println(""Active", ", transaction.isActive()); // false
System.out.println(""Status", ", transaction.status); // "committed'
transaction := client.database.beginTransaction({
"database": "main-db",
});
fmt.Println('"Active": ", transaction.isActive()); // true
transaction.commit();
fmt.Println(""Active": ", transaction.isActive()); // false
fmt.Println(""Status": ", transaction.status); // "committed'
var transaction = await ductape.database.beginTransaction({
["database"] = "main-db",
});
Console.WriteLine('["Active"] = ", transaction.isActive()); // true
await transaction.commit();
Console.WriteLine("["Active"] = ", transaction.isActive()); // false
Console.WriteLine("["Status"] = ", transaction.status); // "committed'
Database-Specific Considerations
PostgreSQL
- Full ACID support
- All isolation levels
- Savepoints supported
- Deferrable transactions for read-only with SERIALIZABLE
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({
...config,
isolationLevel: 'SERIALIZABLE',
readOnly: true,
deferrable: true, // PostgreSQL-specific optimization
}, async (trx) => { ... });
ductape.database.transaction(Map.of(
...config,
"isolationLevel", "SERIALIZABLE",
"readOnly", true,
"deferrable", true, // PostgreSQL-specific optimization
), async (trx) => Map.of( ... ));
client.database.transaction({
...config,
"isolationLevel": "SERIALIZABLE",
"readOnly": true,
"deferrable": true, // PostgreSQL-specific optimization
}, async (trx) => { ... });
await ductape.database.transaction({
...config,
["isolationLevel"] = "SERIALIZABLE",
["readOnly"] = true,
["deferrable"] = true, // PostgreSQL-specific optimization
}, async (trx) => { ... });
MySQL
- Full ACID support (InnoDB)
- All isolation levels
- Savepoints supported
- Default isolation: REPEATABLE_READ
MongoDB
- Requires replica set or sharded cluster
- Savepoints NOT supported
- Higher latency than SQL transactions
- Different isolation semantics
- TypeScript
- Java
- Go
- .NET
// MongoDB transaction
await ductape.database.transaction({
database: 'mongo-db',
}, async (transaction) => {
// Savepoint would throw error here
// await transaction.savepoint('sp1'); // NOT_SUPPORTED error
await ductape.database.insert({
table: 'orders', // Collection name
data: orderData,
transaction,
});
});
// MongoDB transaction
ductape.database.transaction(Map.of(
"database", "mongo-db"
), async (transaction) => Map.of(
// Savepoint would throw error here
// transaction.savepoint('sp1'); // NOT_SUPPORTED error
ductape.database.insert(Map.of(
"table", "orders", // Collection name
data: orderData,
transaction
));
));
// MongoDB transaction
client.database.transaction({
"database": "mongo-db",
}, async (transaction) => {
// Savepoint would throw error here
// transaction.savepoint('sp1'); // NOT_SUPPORTED error
client.database.insert({
"table": "orders", // Collection name
data: orderData,
transaction,
});
});
// MongoDB transaction
await ductape.database.transaction({
["database"] = "mongo-db",
}, async (transaction) => {
// Savepoint would throw error here
// await transaction.savepoint('sp1'); // NOT_SUPPORTED error
await ductape.database.insert({
["table"] = "orders", // Collection name
data: orderData,
transaction,
});
});
DynamoDB
- Uses TransactWriteItems (batch-based)
- Maximum 100 items per transaction
- Savepoints NOT supported
- 2x write cost for transactional writes
- TypeScript
- Java
- Go
- .NET
// DynamoDB transaction (max 100 items)
await ductape.database.transaction({
database: 'dynamo-db',
}, async (transaction) => {
// Each insert/update/delete counts toward 100 item limit
await ductape.database.insert({
table: 'orders',
data: orderData,
transaction,
});
});
// DynamoDB transaction (max 100 items)
ductape.database.transaction(Map.of(
"database", "dynamo-db"
), async (transaction) => Map.of(
// Each insert/update/delete counts toward 100 item limit
ductape.database.insert(Map.of(
"table", "orders",
data: orderData,
transaction
));
));
// DynamoDB transaction (max 100 items)
client.database.transaction({
"database": "dynamo-db",
}, async (transaction) => {
// Each insert/update/delete counts toward 100 item limit
client.database.insert({
"table": "orders",
data: orderData,
transaction,
});
});
// DynamoDB transaction (max 100 items)
await ductape.database.transaction({
["database"] = "dynamo-db",
}, async (transaction) => {
// Each insert/update/delete counts toward 100 item limit
await ductape.database.insert({
["table"] = "orders",
data: orderData,
transaction,
});
});
Transaction Options Reference
| Option | Type | Description |
|---|---|---|
env | string | Environment (dev, staging, prd) |
product | string | Product tag |
database | string | Database tag |
isolationLevel | string | Isolation level |
readOnly | boolean | Read-only optimization |
timeout | number | Timeout in milliseconds |
deferrable | boolean | Deferrable (PostgreSQL only) |
Best Practices
1. Keep Transactions Short
Long transactions hold locks and reduce concurrency:
- TypeScript
- Java
- Go
- .NET
// Good: Short transaction
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ table: 'orders', data, transaction: trx });
await ductape.database.update({ table: 'inventory', data: update, transaction: trx });
});
// Bad: Long transaction with external I/O
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ table: 'orders', data, transaction: trx });
await sendEmailNotification(order); // DON'T do this inside transaction
await callExternalAPI(order); // DON'T do this inside transaction
});
// Good: Short transaction
ductape.database.transaction(Map.of( ... ), async (trx) => Map.of(
ductape.database.insert(Map.of( "table", "orders", data, transaction: trx ));
ductape.database.update(Map.of( "table", "inventory", data: update, transaction: trx ));
));
// Bad: Long transaction with external I/O
ductape.database.transaction(Map.of( ... ), async (trx) => Map.of(
ductape.database.insert(Map.of( "table", "orders", data, transaction: trx ));
sendEmailNotification(order); // DON'T do this inside transaction
callExternalAPI(order); // DON'T do this inside transaction
));
// Good: Short transaction
client.database.transaction({ ... }, async (trx) => {
client.database.insert({ "table": "orders", data, transaction: trx });
client.database.update({ "table": "inventory", data: update, transaction: trx });
});
// Bad: Long transaction with external I/O
client.database.transaction({ ... }, async (trx) => {
client.database.insert({ "table": "orders", data, transaction: trx });
sendEmailNotification(order); // DON'T do this inside transaction
callExternalAPI(order); // DON'T do this inside transaction
});
// Good: Short transaction
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ ["table"] = "orders", data, transaction: trx });
await ductape.database.update({ ["table"] = "inventory", data: update, transaction: trx });
});
// Bad: Long transaction with external I/O
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ ["table"] = "orders", data, transaction: trx });
await sendEmailNotification(order); // DON'T do this inside transaction
await callExternalAPI(order); // DON'T do this inside transaction
});
2. No External I/O in Transactions
Move network calls, file operations, and external API calls outside:
- TypeScript
- Java
- Go
- .NET
// Good: External I/O outside transaction
const order = await ductape.database.transaction({ ... }, async (trx) => {
return await ductape.database.insert({ table: 'orders', data, transaction: trx });
});
// After transaction completes
await sendEmailNotification(order);
await callExternalAPI(order);
// Good: External I/O outside transaction
Map<String, Object> order = ductape.database.transaction(Map.of( ... ), async (trx) => Map.of(
return ductape.database.insert(Map.of( "table", "orders", data, transaction: trx ));
));
// After transaction completes
sendEmailNotification(order);
callExternalAPI(order);
// Good: External I/O outside transaction
order := client.database.transaction({ ... }, async (trx) => {
return client.database.insert({ "table": "orders", data, transaction: trx });
});
// After transaction completes
sendEmailNotification(order);
callExternalAPI(order);
// Good: External I/O outside transaction
var order = await ductape.database.transaction({ ... }, async (trx) => {
return await ductape.database.insert({ ["table"] = "orders", data, transaction: trx });
});
// After transaction completes
await sendEmailNotification(order);
await callExternalAPI(order);
3. Use Callback API
The callback API prevents forgetting to commit or rollback:
- TypeScript
- Java
- Go
- .NET
// Good: Callback API
await ductape.database.transaction({ ... }, async (trx) => {
// Automatic commit on success, rollback on error
});
// Risky: Manual control
const trx = await ductape.database.beginTransaction({ ... });
// If you forget to commit or rollback, connection leaks!
// Good: Callback API
ductape.database.transaction(Map.of( ... ), async (trx) => Map.of(
// Automatic commit on success, rollback on error
));
// Risky: Manual control
Map<String, Object> trx = ductape.database.beginTransaction(Map.of( ... ));
// If you forget to commit or rollback, connection leaks!
// Good: Callback API
client.database.transaction({ ... }, async (trx) => {
// Automatic commit on success, rollback on error
});
// Risky: Manual control
trx := client.database.beginTransaction({ ... });
// If you forget to commit or rollback, connection leaks!
// Good: Callback API
await ductape.database.transaction({ ... }, async (trx) => {
// Automatic commit on success, rollback on error
});
// Risky: Manual control
var trx = await ductape.database.beginTransaction({ ... });
// If you forget to commit or rollback, connection leaks!
4. Choose Appropriate Isolation Level
Use the lowest isolation level that meets your requirements:
- TypeScript
- Java
- Go
- .NET
// Most operations: READ_COMMITTED (default)
await ductape.database.transaction({ ... }, async (trx) => { ... });
// Reports needing consistent reads: REPEATABLE_READ
await ductape.database.transaction({ isolationLevel: 'REPEATABLE_READ' }, async (trx) => { ... });
// Financial/critical: SERIALIZABLE
await ductape.database.transaction({ isolationLevel: 'SERIALIZABLE' }, async (trx) => { ... });
// Most operations: READ_COMMITTED (default)
ductape.database.transaction(Map.of( ... ), async (trx) => Map.of( ... ));
// Reports needing consistent reads: REPEATABLE_READ
ductape.database.transaction(Map.of( "isolationLevel", "REPEATABLE_READ" ), async (trx) => Map.of( ... ));
// Financial/critical: SERIALIZABLE
ductape.database.transaction(Map.of( "isolationLevel", "SERIALIZABLE" ), async (trx) => Map.of( ... ));
// Most operations: READ_COMMITTED (default)
client.database.transaction({ ... }, async (trx) => { ... });
// Reports needing consistent reads: REPEATABLE_READ
client.database.transaction({ "isolationLevel": "REPEATABLE_READ" }, async (trx) => { ... });
// Financial/critical: SERIALIZABLE
client.database.transaction({ "isolationLevel": "SERIALIZABLE" }, async (trx) => { ... });
// Most operations: READ_COMMITTED (default)
await ductape.database.transaction({ ... }, async (trx) => { ... });
// Reports needing consistent reads: REPEATABLE_READ
await ductape.database.transaction({ ["isolationLevel"] = "REPEATABLE_READ" }, async (trx) => { ... });
// Financial/critical: SERIALIZABLE
await ductape.database.transaction({ ["isolationLevel"] = "SERIALIZABLE" }, async (trx) => { ... });
5. Use Savepoints for Partial Operations
When some operations can fail without killing the entire transaction:
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ table: 'main_record', data, transaction: trx });
const sp = await trx.savepoint('optional_data');
try {
await ductape.database.insert({ table: 'optional_record', data, transaction: trx });
await sp.release();
} catch {
await sp.rollback(); // Main record still saved
}
});
ductape.database.transaction(Map.of( ... ), async (trx) => Map.of(
ductape.database.insert(Map.of( "table", "main_record", data, transaction: trx ));
Map<String, Object> sp = trx.savepoint('optional_data');
try Map.of(
ductape.database.insert(Map.of( "table", "optional_record", data, transaction: trx ));
sp.release();
) catch Map.of(
sp.rollback(); // Main record still saved
)
));
client.database.transaction({ ... }, async (trx) => {
client.database.insert({ "table": "main_record", data, transaction: trx });
sp := trx.savepoint('optional_data');
try {
client.database.insert({ "table": "optional_record", data, transaction: trx });
sp.release();
} catch {
sp.rollback(); // Main record still saved
}
});
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ ["table"] = "main_record", data, transaction: trx });
var sp = await trx.savepoint('optional_data');
try {
await ductape.database.insert({ ["table"] = "optional_record", data, transaction: trx });
await sp.release();
} catch {
await sp.rollback(); // Main record still saved
}
});
Next Steps
- Migrations - Schema changes within transactions
- Best Practices - Production patterns
- Direct Queries - Raw queries in transactions