Service Bus Transactions

Azure Service Bus Transactions allow multiple messaging operations to succeed or fail together as a single atomic unit. If any operation in the transaction fails, all other operations in the same transaction are rolled back automatically. No partial state is left behind. This is essential for workflows where two or more messaging actions must happen together or not at all.

What Is a Transaction in Service Bus?

A transaction groups a set of operations — such as completing a received message and sending a new message — into one atomic action. Either all operations commit or all roll back. This is the same concept as database transactions but applied to message operations.

WITHOUT Transaction:

Step 1: Complete received message "Order-101"  --> SUCCESS
Step 2: Send new message "Invoice-101"          --> FAILS (error)

Result: Order-101 is GONE (completed) but Invoice-101 never sent.
System is in inconsistent state.
WITH Transaction:

Begin Transaction
Step 1: Complete received message "Order-101"  --> staged
Step 2: Send new message "Invoice-101"          --> staged
Commit Transaction

If both succeed --> Both committed atomically.
If Step 2 fails --> Entire transaction rolls back.
                    Order-101 is NOT completed (it stays in the queue).
                    Invoice-101 is NOT sent.
System stays consistent.

Supported Transactional Operations

OperationTransactional?
Complete a messageYes
Abandon a messageYes
Dead-letter a messageYes
Defer a messageYes
Send a message to a queueYes
Send a message to a topicYes
Schedule a messageYes
Cancel a scheduled messageYes

Transaction Scope — Via Entity Constraint

Service Bus transactions operate within a Via Entity. All transactional operations must go through one entity (the via entity). A transaction cannot span operations on completely unrelated, unconnected entities unless one is the via entity.

Valid transaction scope (single entity or via entity):
  - Receive from Queue A AND send to Queue B (via Queue A)
  - Complete from Queue A AND send to Queue A (same entity)
  - Receive from Queue A AND send to Topic X (via Queue A)

Invalid (crossing namespaces):
  - Operations across two different namespaces in one transaction

Basic Transaction — .NET SDK

Pattern: Receive a Message and Send a New Message Atomically

using Azure.Messaging.ServiceBus;

await using var client = new ServiceBusClient(connectionString);

// Receiver for the order queue
await using var receiver = client.CreateReceiver("order-queue");

// Sender for the invoice queue (via entity = order-queue)
await using var sender = client.CreateSender("invoice-queue");

// Receive the order message
var orderMsg = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));

if (orderMsg != null)
{
    Console.WriteLine($"Order received: {orderMsg.Body}");

    // Create the invoice message to send
    var invoiceMsg = new ServiceBusMessage($"Invoice for: {orderMsg.Body}")
    {
        MessageId = $"invoice-{orderMsg.MessageId}"
    };

    // Begin transaction
    using var transaction = new ServiceBusTransactionContext();

    try
    {
        // Complete the order message (inside transaction)
        await receiver.CompleteMessageAsync(orderMsg, transaction);

        // Send invoice message (inside transaction)
        await sender.SendMessageAsync(invoiceMsg, transaction);

        // Commit — both operations commit atomically
        await transaction.CommitAsync();
        Console.WriteLine("Transaction committed. Order completed and invoice sent.");
    }
    catch (Exception ex)
    {
        // Rollback — neither operation takes effect
        await transaction.RollbackAsync();
        Console.WriteLine($"Transaction rolled back: {ex.Message}");
    }
}

Transaction via Entity — Correct Setup

When sending to a different queue inside a transaction, the sender must use the via entity path. The via entity is the queue the receiver is connected to. This tells Service Bus to route the send operation through the receive entity for transactional consistency.

// Correct setup: sender uses via-entity path
await using var receiver = client.CreateReceiver("order-queue");

// ViaQueueName ensures the send operation goes through order-queue transactionally
await using var sender = client.CreateSender("invoice-queue");

// Use TransactionContext tied to the receiver's entity

Transaction — Practical Example: Order to Payment

Scenario:
  1. Read "OrderPlaced" message from order-queue
  2. Send "ProcessPayment" message to payment-queue
  3. Complete the "OrderPlaced" message

All three steps must succeed together. If payment queue fails, order stays in queue.

await using var orderReceiver  = client.CreateReceiver("order-queue");
await using var paymentSender  = client.CreateSender("payment-queue");

var orderMsg = await orderReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));

if (orderMsg != null)
{
    var paymentMsg = new ServiceBusMessage($"Pay for order: {orderMsg.MessageId}")
    {
        MessageId = $"pay-{orderMsg.MessageId}"
    };

    using ServiceBusTransactionContext txn = new();

    try
    {
        await paymentSender.SendMessageAsync(paymentMsg, txn);
        await orderReceiver.CompleteMessageAsync(orderMsg, txn);
        await txn.CommitAsync();

        Console.WriteLine("Order completed and payment triggered atomically.");
    }
    catch
    {
        await txn.RollbackAsync();
        Console.WriteLine("Transaction rolled back. Order message preserved.");
    }
}

Transaction with Multiple Sends

// Send two messages atomically — both go or neither goes
using ServiceBusTransactionContext txn = new();

try
{
    var msg1 = new ServiceBusMessage("Notification: Email");
    var msg2 = new ServiceBusMessage("Notification: SMS");

    await emailSender.SendMessageAsync(msg1, txn);
    await smsSender.SendMessageAsync(msg2, txn);

    await txn.CommitAsync();
    Console.WriteLine("Both notifications sent atomically.");
}
catch
{
    await txn.RollbackAsync();
    Console.WriteLine("Transaction failed. No notifications sent.");
}

Transaction Rules and Limitations

RuleDetail
Via Entity RequiredAll sends to a different entity must use the via entity (receiver's entity)
Same Namespace OnlyTransactions cannot span two different namespaces
Max OperationsUp to 100 operations per transaction
No Receives in TransactionReceiving a message starts a lock but is not part of the transaction scope. Only settlement operations (Complete/Abandon) are transactional.
Tier SupportTransactions are available in Standard and Premium tiers. Not in Basic.

Transactions vs Message Locks

Message Lock:
  Consumer reads message --> Lock starts (e.g., 60 seconds)
  Lock prevents other consumers from reading the same message during processing
  Lock expires --> message returns to queue

Transaction:
  Wraps settlement operations + sends
  All operations commit or all rollback atomically
  Transaction rollback returns the received message to the queue automatically

Best Practices for Service Bus Transactions

  • Keep transactions short — long-running transactions risk message lock expiry
  • Always wrap transactions in try-catch-rollback to handle failures cleanly
  • Use transactions when exactly-once delivery semantics are a strict requirement
  • Avoid mixing non-Service Bus operations (database calls) inside Service Bus transactions — they have separate commit/rollback mechanisms
  • Use the outbox pattern for cross-system atomicity (database + Service Bus)

Outbox Pattern — Cross-System Atomicity

Problem: Need to update database AND send message atomically
         (Service Bus and SQL have different transaction managers)

Outbox Pattern:
  Step 1: Write event to database outbox table (same DB transaction as business data)
  Step 2: A background worker reads the outbox table
  Step 3: Worker sends the event to Service Bus
  Step 4: Worker marks the outbox record as "sent"

Result: Database and Service Bus are eventually consistent without a distributed transaction.

Summary

Azure Service Bus Transactions provide atomicity across multiple messaging operations. A group of operations — such as completing a received message and sending a new one — either all succeed or all roll back. Transactions require a via entity to ensure all operations flow through one entity for consistency. They work within a single namespace and support up to 100 operations per transaction. Transactions are available in Standard and Premium tiers and are essential for building reliable, consistent workflows where partial execution is not acceptable.

Leave a Comment