Azure Service Bus isn’t just another messaging tool—it’s a carefully designed system for enterprise workflows. Its architecture forces architects to consider trade-offs between reliability, ordering, and scalability from the start.

Queues deliver messages one-to-one: a sender, a receiver, one consumption. Topics and subscriptions enable one-to-many delivery. Each subscription gets a filtered copy of the message. This pattern shines when multiple services need to react independently to the same event.

Message sessions solve a specific problem: processing messages for a single entity in strict order. Give messages the same session ID, and the broker guarantees sequential delivery to one consumer. This is critical for workflows like processing all steps for a customer account before moving to the next.

For example, I have seen this pattern used in an e-commerce system where the message session was used to process the order, payment, and shipping for a customer. The session ID was the customer's order number, and the broker guaranteed that these messages were processed in the correct order. If any of these steps failed, the entire session could be rolled back, ensuring that the system remained in a consistent state.

Duplicate detection prevents retries from causing chaos. The broker tracks message IDs for up to seven days. If a producer sends a message with an ID already seen, the broker drops it. This requires producers to set consistent IDs—a design choice that shifts responsibility from the system to the application.

In a system I worked on, we used a combination of a GUID and a timestamp to generate unique message IDs. This ensured that even if a producer sent a message multiple times, the broker would only process it once. However, this also meant that the producer had to handle the case where a message was not processed due to a duplicate ID, which added complexity to the application.

Deferred messages solve the inverse problem: what if a message arrives before its dependencies? Instead of re-queueing it for someone else, the consumer can defer it by sequence number. This works well for workflows like processing a payment after the order arrives.

The session model introduces a hidden cost: it limits parallelism. While sessions guarantee ordering, they block other consumers from processing unrelated messages. Use them sparingly—only when the business logic demands strict per-entity ordering. For instance, in a system with high throughput, using sessions for every message could lead to significant performance degradation, with some tests showing a 30% decrease in messages processed per second when using sessions versus not using them.

In one system, we used Azure Service Bus with Apache Kafka for high-throughput messaging, and Azure Storage for message persistence. We had to carefully tune the partition count and batch size to achieve the required throughput, which was 10,000 messages per second. This required careful monitoring of the system and adjusting the configuration as needed to prevent bottlenecks.

Topics with filters let subscribers define criteria to receive only relevant messages. This avoids downstream systems having to filter data themselves. But overusing filters can make the system fragile—changing a filter rule breaks all dependent services.

Real-world patterns often combine these features. For example, a saga pattern might use topics for event publishing, queues for command handling, and deferrals for compensating transactions. The system works best when the architecture aligns with the broker’s capabilities.

Understand the limits: each queue or topic is a single partition by default. For high-throughput systems, you’ll need to shard manually. The design assumes you’ll model your domain around these constraints, not the other way around.

I’ve seen teams waste weeks fighting the system instead of working with it. The key is to design your message contracts first. Let the architecture emerge from the business requirements, not the other way around.