You can talk to Azure Service Bus directly with its SDK. Most teams outgrow that quickly — retry policies, the outbox pattern, saga orchestration, and testability all end up hand-rolled and duplicated across services. MassTransit wraps the transport in a consistent abstraction so your message contracts and consumers stay the same even if the transport underneath changes.

Key takeaways
  • MassTransit gives you publish/consume, retries, and error queues as configuration, not code you write per project.
  • Define message contracts as interfaces, not concrete classes, and only add fields — that discipline is what keeps consumers forward-compatible.
  • Use the transactional outbox so a message publish is atomic with the database write that triggered it.
  • Because MassTransit abstracts the transport, moving from Azure Service Bus to RabbitMQ later is mostly a configuration change, not a rewrite.

Setup

csharp
builder.Services.AddMassTransit(x =>
{
    x.AddConsumer();

    x.UsingAzureServiceBus((context, cfg) =>
    {
        cfg.Host(builder.Configuration.GetConnectionString("ServiceBus"));
        cfg.ConfigureEndpoints(context);
    });
});

Publishing an event

Contracts are plain interfaces. That's deliberate — it keeps the message shape decoupled from any single assembly's concrete implementation and makes versioning additive rather than breaking.

csharp
public interface OrderPlaced
{
    Guid OrderId { get; }
    string CustomerEmail { get; }
    decimal Total { get; }
    DateTimeOffset PlacedAt { get; }
}

public class OrderService(IPublishEndpoint publisher)
{
    public async Task PlaceOrderAsync(Order order, CancellationToken ct)
    {
        await publisher.Publish(new
        {
            OrderId = order.Id,
            order.CustomerEmail,
            order.Total,
            PlacedAt = DateTimeOffset.UtcNow
        }, ct);
    }
}

Consuming it

csharp
public class OrderPlacedConsumer(IEmailSender email, ILogger logger)
    : IConsumer
{
    public async Task Consume(ConsumeContext context)
    {
        var message = context.Message;
        logger.LogInformation("Sending confirmation for order {OrderId}", message.OrderId);
        await email.SendOrderConfirmationAsync(message.CustomerEmail, message.OrderId, context.CancellationToken);
    }
}

The outbox: avoiding the dual-write problem

Saving an order to the database and publishing OrderPlaced are two separate operations. If the process crashes between them, you either lose the event or send it for an order that never actually committed. The transactional outbox writes the message to the same database transaction as your business data, then a background process delivers it — making the publish atomic with the write that caused it.

csharp
builder.Services.AddMassTransit(x =>
{
    x.AddEntityFrameworkOutbox(o =>
    {
        o.UseSqlServer();
        o.UseBusOutbox();
    });

    x.AddConsumer();
    x.UsingAzureServiceBus((context, cfg) => cfg.ConfigureEndpoints(context));
});

Retries and error queues

MassTransit retries a failing consumer automatically, then moves the message to a conventionally named _error queue once retries are exhausted — instead of losing it or blocking the queue.

csharp
cfg.ConfigureEndpoints(context, endpointNameFormatter);
cfg.UseMessageRetry(r => r.Exponential(
    retryLimit: 5,
    minInterval: TimeSpan.FromSeconds(1),
    maxInterval: TimeSpan.FromMinutes(1),
    intervalDelta: TimeSpan.FromSeconds(2)));
Going furtherFor multi-step workflows — reserve inventory, charge payment, ship order, with compensation if any step fails — MassTransit's state machine sagas are worth a look. They're a concrete implementation of the orchestration pattern we covered in our event-driven architecture piece, without hand-rolling a state machine yourself.

The payoff for this abstraction shows up later, not on day one: when a client needs on-premises RabbitMQ instead of Azure Service Bus for compliance reasons, or you consolidate two systems onto one transport, the change is almost entirely in the UsingAzureServiceBus configuration block — your consumers and contracts don't move.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.