Transactions in Practice
Apply transactional patterns to real scenarios β order processing with TransactionalBatch, inventory reservation with stored procedures, and cross-partition consistency with the Saga pattern.
Transactions in the real world
Think of buying a coffee. Three things must happen: (1) your card gets charged, (2) the coffee is poured, (3) the receipt is printed. If the card fails, you donβt get coffee and no receipt is printed. Thatβs a transaction β all or nothing.
In Cosmos DB, you have three ways to get βall or nothingβ: TransactionalBatch (simple, SDK-native), stored procedures (flexible, JavaScript), and Sagas (for cross-partition work where true transactions arenβt possible).
Pattern 1: Order processing with TransactionalBatch
π Priyaβs NovaSaaS billing system needs to atomically: create an order, create a payment record, and update the customerβs subscription status β all in the same tenant partition.
// All three items share the same partition key: /tenantId = "tenant-abc"
var order = new OrderItem
{
Id = "ord-500",
TenantId = "tenant-abc",
Type = "order",
Amount = 299.00m,
Status = "confirmed",
CreatedAt = DateTime.UtcNow
};
var payment = new PaymentItem
{
Id = "pay-500",
TenantId = "tenant-abc",
Type = "payment",
OrderId = "ord-500",
Method = "credit-card",
Status = "captured"
};
TransactionalBatchResponse batch = await container
.CreateTransactionalBatch(new PartitionKey("tenant-abc"))
.CreateItem(order)
.CreateItem(payment)
.PatchItem("sub-001", new[]
{
PatchOperation.Set("/status", "active"),
PatchOperation.Set("/lastPaymentDate", DateTime.UtcNow.ToString("o"))
})
.ExecuteAsync();
if (batch.IsSuccessStatusCode)
{
Console.WriteLine("Order, payment, and subscription updated atomically.");
}
else
{
// ALL operations rolled back β nothing was written
Console.WriteLine($"Transaction failed: {batch.StatusCode}");
TransactionalBatchOperationResult failedOp = batch[batch.Count - 1];
Console.WriteLine($"Failed operation: {failedOp.StatusCode}");
}
Why TransactionalBatch here: All items share /tenantId, the operations are simple CRUD, and we need atomicity. No JavaScript needed.
Pattern 2: Inventory reservation with stored procedure
Jakeβs GlobeCart e-commerce platform needs a stored procedure that reads the current stock, checks availability, and reserves items β all atomically:
function reserveInventory(productId, quantity) {
var context = getContext();
var container = context.getCollection();
var response = context.getResponse();
// Step 1: Read current inventory
var query = 'SELECT * FROM c WHERE c.id = "' + productId + '" AND c.type = "inventory"';
var accepted = container.queryDocuments(container.getSelfLink(), query,
function (err, docs) {
if (err) throw new Error("Query failed: " + err.message);
if (docs.length === 0) throw new Error("Product not found: " + productId);
var inventory = docs[0];
// Step 2: Check availability
if (inventory.availableStock < quantity) {
throw new Error("Insufficient stock. Available: " + inventory.availableStock +
", Requested: " + quantity);
}
// Step 3: Reserve (atomic β if this fails, the read is also rolled back)
inventory.availableStock -= quantity;
inventory.reservedStock += quantity;
inventory.lastReservedAt = new Date().toISOString();
var replaceAccepted = container.replaceDocument(inventory._self, inventory,
function (err) {
if (err) throw new Error("Reserve failed: " + err.message);
response.setBody({
success: true,
remainingStock: inventory.availableStock,
reservedQuantity: quantity
});
}
);
if (!replaceAccepted) throw new Error("Replace not accepted");
}
);
if (!accepted) throw new Error("Query not accepted");
}
// Calling from C#
var result = await container.Scripts.ExecuteStoredProcedureAsync<dynamic>(
"reserveInventory",
new PartitionKey("product-laptop-001"),
new dynamic[] { "product-laptop-001", 2 });
Console.WriteLine($"Reservation: {result.Resource}");
Why a stored procedure here: We need a read-check-write pattern β read stock, validate, then write. TransactionalBatch canβt express conditional logic (itβs a flat list of operations). The stored procedure ensures no race condition between the read and write.
Pattern 3: Cross-partition Saga
π‘ Amara at SensorFlow needs to provision a new IoT device β which involves writing to three different containers with different partition keys:
1. Create device record β devices container (pk: /deviceId)
2. Assign to fleet β fleets container (pk: /fleetId)
3. Create monitoring config β configs container (pk: /deviceId)
These canβt be in a single TransactionalBatch (different containers/partitions). The Saga pattern handles this:
// Saga: provision a new device with compensating transactions
string deviceId = "sensor-5001";
try
{
// Step 1: Create device
await devicesContainer.CreateItemAsync(device, new PartitionKey(deviceId));
try
{
// Step 2: Assign to fleet
await fleetsContainer.PatchItemAsync<FleetItem>(
"fleet-industrial",
new PartitionKey("fleet-industrial"),
new[] { PatchOperation.Add("/devices/-", deviceId) });
try
{
// Step 3: Create monitoring config
await configsContainer.CreateItemAsync(config, new PartitionKey(deviceId));
Console.WriteLine("Device provisioned successfully.");
}
catch
{
// Compensate: remove from fleet
await fleetsContainer.PatchItemAsync<FleetItem>(
"fleet-industrial",
new PartitionKey("fleet-industrial"),
new[] { PatchOperation.Remove($"/devices/{fleetDeviceIndex}") });
throw;
}
}
catch
{
// Compensate: delete device
await devicesContainer.DeleteItemAsync<DeviceItem>(deviceId, new PartitionKey(deviceId));
throw;
}
}
catch (Exception ex)
{
Console.WriteLine($"Provisioning failed. All changes compensated. Error: {ex.Message}");
}
Saga trade-offs:
- Not truly atomic β thereβs a window where partial state is visible
- Compensating transactions can also fail β you need retry logic and dead-letter queues
- More complex β but itβs the only option for cross-partition/cross-container consistency
Comparing the three patterns
| Aspect | TransactionalBatch | Stored procedure | Saga |
|---|---|---|---|
| Scope | Single partition, single container | Single partition, single container | Cross-partition, cross-container |
| Atomicity | β True ACID β all or nothing | β True ACID β all or nothing | β οΈ Eventual β compensating transactions |
| Language | C# / Java SDK | JavaScript (server-side) | C# / Java SDK (client-side orchestration) |
| Conditional logic | β No β flat list of operations | β Yes β read, validate, branch | β Yes β full client-side logic |
| Max operations | 100 operations, 2 MB | 5-second timeout | No hard limit |
| Complexity | Low | Medium | High |
| Best for | Multi-item CRUD in same partition | Read-check-write (e.g., inventory) | Cross-partition workflows |
Exam tip: when batch can't help
TransactionalBatch is flat β you canβt read an item, check a condition, and then decide what to write. If the question describes a read-then-decide-then-write pattern, the answer is a stored procedure (within one partition) or a Saga (across partitions). Batch is for known operations that donβt depend on reads.
Exam tip: saga is not ACID
The Saga pattern provides eventual consistency, not ACID atomicity. Thereβs always a window where partial state is visible. Compensating transactions may also fail, requiring idempotent operations and retry logic. The exam tests this distinction β if a question requires strict ACID across partitions, the answer is βnot possible with Cosmos DBβ (redesign your data model to put the data in one partition).
π¬ Video walkthrough
π¬ Video coming soon
Transactions in Practice β DP-420 Module 11
Transactions in Practice β DP-420 Module 11
~12 minFlashcards
Knowledge check
Jake's GlobeCart needs to: (1) check product stock, (2) reserve inventory if available, (3) reject if out of stock. All data is in the same partition. Which approach is best?
Amara needs to write data to three different containers atomically. What's her best option?
During a Saga, step 2 of 3 succeeds but step 3 fails. The compensating transaction for step 2 also fails. What should happen?
Next up: Domain 2 β dive into indexing policies, consistency levels, and throughput optimisation to make your Cosmos DB data model perform at scale.