Service Bus Interview Questions and Answers Part 1 - Service Bus Fundamentals and Architecture

Service Bus Interview Q&A · Part 1 of 10

In this post we are going to cover Azure Service Bus fundamentals and architecture. These are 7 real Service Bus interview questions for data engineers, asked the way they come up on a real project, and the last one is a scenario: the business wants every online order processed reliably by billing, shipping and email, even when one of them is down, and you design it.

Service Bus is the message broker in Azure. One service hands a message to it and moves on, and another service picks the message up when it's ready. Interviewers start here because every later question builds on it: namespaces, queues, topics, subscriptions, what a message carries and which tier to pick. If you can explain why the order service keeps working while billing is down, you show that you understand why a broker exists.

Part 1 of the Real Time Azure Service Bus Interview Questions and Answers series on TechBrothersIT.

The short answer

Say this first

Azure Service Bus is a fully managed enterprise message broker with queues (one consumer per message) and topics whose subscriptions each get a copy, all inside a namespace. It decouples producers from consumers, delivers at least once, and Standard or Premium adds topics, sessions, duplicate detection and transactions.

Let's set up the project

We run Microsoft's official Service Bus emulator locally, with SQL Server next to it, and small Python scripts that use the azure-servicebus client, so every output is real. Features the emulator doesn't support are shown as code. The emulator reads its entities from Config.json: the queues invoices, orders and payments, and the topic order-events with the subscriptions billing, shipping and email. shop.py is a small helper: client() connects with the SERVICEBUS_CONNECTION_STRING environment variable, and props() decodes application properties.

Config.json
{
  "UserConfig": {
    "Namespaces": [
      {
        "Name": "sbemulatorns",
        "Queues": [
          {
            "Name": "invoices",
            "Properties": {
              "DeadLetteringOnMessageExpiration": false,
              "DefaultMessageTimeToLive": "PT1H",
              "DuplicateDetectionHistoryTimeWindow": "PT20S",
              "ForwardDeadLetteredMessagesTo": "",
              "ForwardTo": "",
              "LockDuration": "PT1M",
              "MaxDeliveryCount": 3,
              "RequiresDuplicateDetection": false,
              "RequiresSession": false
            }
          },
          {
            "Name": "orders",
            "Properties": {
              "DeadLetteringOnMessageExpiration": false,
              "DefaultMessageTimeToLive": "PT1H",
              "DuplicateDetectionHistoryTimeWindow": "PT20S",
              "ForwardDeadLetteredMessagesTo": "",
              "ForwardTo": "",
              "LockDuration": "PT1M",
              "MaxDeliveryCount": 3,
              "RequiresDuplicateDetection": false,
              "RequiresSession": false
            }
          },
          {
            "Name": "payments",
            "Properties": {
              "DeadLetteringOnMessageExpiration": false,
              "DefaultMessageTimeToLive": "PT1H",
              "DuplicateDetectionHistoryTimeWindow": "PT20S",
              "ForwardDeadLetteredMessagesTo": "",
              "ForwardTo": "",
              "LockDuration": "PT1M",
              "MaxDeliveryCount": 3,
              "RequiresDuplicateDetection": false,
              "RequiresSession": false
            }
          }
        ],
        "Topics": [
          {
            "Name": "order-events",
            "Properties": {
              "DefaultMessageTimeToLive": "PT1H",
              "DuplicateDetectionHistoryTimeWindow": "PT20S",
              "RequiresDuplicateDetection": false
            },
            "Subscriptions": [
              {
                "Name": "billing",
                "Properties": {
                  "DeadLetteringOnMessageExpiration": false,
                  "DefaultMessageTimeToLive": "PT1H",
                  "LockDuration": "PT1M",
                  "MaxDeliveryCount": 3,
                  "ForwardDeadLetteredMessagesTo": "",
                  "ForwardTo": "",
                  "RequiresSession": false
                }
              },
              {
                "Name": "shipping",
                "Properties": {
                  "DeadLetteringOnMessageExpiration": false,
                  "DefaultMessageTimeToLive": "PT1H",
                  "LockDuration": "PT1M",
                  "MaxDeliveryCount": 3,
                  "ForwardDeadLetteredMessagesTo": "",
                  "ForwardTo": "",
                  "RequiresSession": false
                }
              },
              {
                "Name": "email",
                "Properties": {
                  "DeadLetteringOnMessageExpiration": false,
                  "DefaultMessageTimeToLive": "PT1H",
                  "LockDuration": "PT1M",
                  "MaxDeliveryCount": 3,
                  "ForwardDeadLetteredMessagesTo": "",
                  "ForwardTo": "",
                  "RequiresSession": false
                }
              }
            ]
          }
        ]
      }
    ],
    "Logging": {
      "Type": "File"
    }
  }
}
shop.py
import os
from azure.servicebus import ServiceBusClient


def client():
    return ServiceBusClient.from_connection_string(
        os.environ["SERVICEBUS_CONNECTION_STRING"])


def props(msg):
    """Application properties: keys and str values arrive as bytes."""
    raw = msg.application_properties or {}
    return {k.decode(): v.decode() if isinstance(v, bytes) else v
            for k, v in raw.items()}

Question 1: Your manager asks what Azure Service Bus is and why the team needs a message broker. What do you say?

Azure Service Bus is a fully managed enterprise message broker, with queues and publish-subscribe topics. Its job is to decouple applications and services from each other.

Let's take an order service and a billing service. Instead of calling billing directly, the order service hands a message to Service Bus and moves on. Service Bus stores it until billing receives it, so the two don't have to be online at the same time.

  • Order service - producer: sends and moves on
  • Service Bus - stores the message
  • Billing - consumer: receives when ready

That one idea gives us four things:

  • Decoupling - producer and consumer need not be online at the same time
  • Load leveling - the consumer handles the average load, not the peak
  • Competing consumers - several receivers share one queue
  • Topics - one message fans out to many subscribers

On top come the enterprise features: dead-letter queues, sessions for ordering, duplicate detection, scheduled messages and transactions. The main protocol is AMQP 1.0, an open ISO/IEC standard.

The pitfall I see

People expect exactly once. Service Bus delivers at least once, so a message can arrive twice. Design every consumer to be idempotent from day one.

Question 2: A new teammate asks how namespaces, queues, topics and subscriptions fit together. How do you explain it?

A namespace is the container for the messaging components, with one endpoint. Inside it are queues and topics. A queue is one-to-one: each message goes to one consumer. A topic is one-to-many: every subscription gets its own copy.

Think of a subscription as a virtual queue on the topic. On the receiver side it works exactly like a queue, and its rules can filter which messages it takes.

Let's see both. structure.py sends one invoice to the invoices queue and one order to the order-events topic. Then two workers read the same queue at the same time, and each subscription receives. We run python structure.py.

structure.py
import shop
from azure.servicebus import ServiceBusMessage

with shop.client() as sbc:
    with sbc.get_queue_sender("invoices") as sender:
        sender.send_messages(ServiceBusMessage("invoice inv-501"))
    with sbc.get_topic_sender("order-events") as sender:
        sender.send_messages(ServiceBusMessage("order 1001 placed"))
    with sbc.get_queue_receiver("invoices") as w1, \
            sbc.get_queue_receiver("invoices") as w2:  # two workers
        got1 = w1.receive_messages(max_wait_time=3)  # locks it
        got2 = w2.receive_messages(max_wait_time=3)  # same queue
        for msg in got1: w1.complete_message(msg)
    print("queue invoices: worker-1 got", [str(m) for m in got1])
    print("queue invoices: worker-2 got", [str(m) for m in got2])
    for sub in ["billing", "shipping", "email"]:  # a copy each
        with sbc.get_subscription_receiver("order-events", sub) as r:
            got = r.receive_messages(max_wait_time=3)
            for msg in got: r.complete_message(msg)
        print("subscription", sub, "got", [str(m) for m in got])
Terminal
$ python structure.py
queue invoices: worker-1 got ['invoice inv-501']
queue invoices: worker-2 got []
subscription billing got ['order 1001 placed']
subscription shipping got ['order 1001 placed']
subscription email got ['order 1001 placed']

Worker 1 got ['invoice inv-501'] and locked it, so worker 2, on the same queue, got []. On the topic, billing, shipping and email each got ['order 1001 placed']: one send, three copies.

In Azure, we create that hierarchy with the Azure CLI: a namespace, a queue, a topic, and subscriptions on the topic. The emulator builds them from its config file.

create.sh
# Azure: the same hierarchy, created with the Azure CLI
az servicebus namespace create -g rg-shop -n sb-shop-prod \
  --sku Standard --location eastus
az servicebus queue create -g rg-shop --namespace-name sb-shop-prod \
  -n invoices
az servicebus topic create -g rg-shop --namespace-name sb-shop-prod \
  -n order-events
az servicebus topic subscription create -g rg-shop \
  --namespace-name sb-shop-prod --topic-name order-events -n billing
A tip

Senders only know the topic, never the subscriptions. So a new team adds its own subscription, and the publisher doesn't change.

Question 3: Someone asks when to use Service Bus, Event Hubs, Event Grid or Storage queues. How do you choose?

Start with what you're moving. Service Bus is enterprise transactional messaging, for high-value messages like orders. Event Hubs is big data streaming, for telemetry. Event Grid is reactive event routing, for notifications.

Let's compare the four.

Service BusEvent HubsEvent GridStorage queues
Built forenterprise transactional messagingbig data streaming and ingestionreactive event routingsimple, very large queues
Carriesmessages (high-value payloads)event streams (time-ordered)events (discrete notifications)messages up to 64 KB
OrderingFIFO with sessionsper partitionno guaranteeno guarantee
Dead-letteringyesnoyesnot automatic
Duplicate detection, transactionsyesnonono

The features follow from that. Service Bus has ordering with sessions, dead-lettering, duplicate detection and transactions. Event Hubs orders per partition, with no dead-lettering, and Event Grid has no ordering guarantee.

Microsoft puts the difference simply:

  • Event - a lightweight notification; the publisher has no expectation about how it is handled
  • Message - raw data, with a contract between publisher and consumer

Storage queues are the simple option: messages up to 64 KB, no ordering guarantee, no automatic dead-lettering, but huge backlogs. Service Bus queues are 1 GB to 80 GB, so I pick Storage queues when a queue must hold more than 80 GB, or when I need progress tracking.

Real project

A real shop often uses three of them: Service Bus to process orders, Event Hubs to capture site telemetry, and Event Grid to react when an item ships.

Question 4: The order service calls the billing service directly and fails when billing is down. How does a queue decouple them?

Put a queue between them. With a direct call, the order service needs billing up right now. With a queue in between, it only needs Service Bus, and billing reads the queue whenever it's running.

Let's try both. First the direct call with python checkout_direct.py. Billing's API isn't running. Then the queue version: python checkout.py 1001 1002 1003 sends three orders, each with its order ID as the message ID, and python waiting.py orders peeks at the queue. Later billing starts with python billing.py, and we peek again.

checkout_direct.py
import urllib.request

order_id = "1001"
try:                       # billing's HTTP API: down right now
    urllib.request.urlopen(
        f"http://localhost:8099/bill?order={order_id}", timeout=3)
    print("order", order_id, "billed")
except OSError as e:
    print("order", order_id, "FAILED at checkout:",
          type(getattr(e, "reason", e)).__name__)
checkout.py
import json, sys, shop
from azure.servicebus import ServiceBusMessage

with shop.client() as sbc, sbc.get_queue_sender("orders") as sender:
    for order_id in sys.argv[1:]:
        body = json.dumps({"order_id": order_id, "total": 49.90})
        msg = ServiceBusMessage(body, message_id=order_id,
                                content_type="application/json")
        sender.send_messages(msg)   # returns after the broker's ack
        print("order", order_id, "accepted: queued for billing")
waiting.py
import sys, shop

queue = sys.argv[1]
with shop.client() as sbc, sbc.get_queue_receiver(queue) as receiver:
    msgs = receiver.peek_messages(max_message_count=100,
                                  sequence_number=1)   # no lock
ids = [m.message_id for m in msgs]
print("waiting in", queue + ":", len(msgs), ids)
billing.py
import json, shop

with shop.client() as sbc, sbc.get_queue_receiver(
        "orders") as receiver:                     # peek-lock mode
    for msg in receiver.receive_messages(max_message_count=10,
                                         max_wait_time=3):
        order = json.loads(str(msg))
        print("billing: invoiced order", order["order_id"],
              "| seq", msg.sequence_number)
        receiver.complete_message(msg)      # done: remove it
Terminal
$ python checkout_direct.py
order 1001 FAILED at checkout: ConnectionRefusedError
$ python checkout.py 1001 1002 1003
order 1001 accepted: queued for billing
order 1002 accepted: queued for billing
order 1003 accepted: queued for billing
$ python waiting.py orders
waiting in orders: 3 ['1001', '1002', '1003']
$ python billing.py
billing: invoiced order 1001 | seq 1
billing: invoiced order 1002 | seq 2
billing: invoiced order 1003 | seq 3
$ python waiting.py orders
waiting in orders: 0 []

The direct call gets order 1001 FAILED at checkout: ConnectionRefusedError. Through the queue, all three orders print accepted: queued for billing while billing is down. The send returns after Service Bus acknowledges it. The peek shows waiting in orders: 3 ['1001', '1002', '1003'].

Then billing receives in peek-lock mode, invoices each order and completes it only after the work: seq 1, seq 2 and seq 3, in send order. The last peek shows waiting in orders: 0 [].

That's temporal decoupling, with load leveling for free.

The pitfall

The send itself can still fail. The client retries, but the order service must handle a final failure, not swallow it.

Question 5: A teammate asks what is actually inside a Service Bus message. How do you explain body, system and application properties?

A message has three parts. The body is the payload, an opaque binary block to Service Bus, so we set a content type. System properties are standard fields: we set some, the broker sets others. Application properties are our own key-value pairs.

PartExamplesWho sets it
BodyJSON payment, as bytesyour app
System propertiesmessage_id, subject, content_type, correlation_idyour app
System properties (read-only)sequence_number, delivery_count, enqueued_time_utcthe broker
Application propertiesregion, attempt, vipyour app

Let's build one. anatomy.py sends a payment with a message ID, a subject, a content type, and a correlation ID pointing back to order 1001, plus three application properties: region, attempt and vip. Then it receives it and prints every part. We run python anatomy.py.

anatomy.py
import json, shop
from azure.servicebus import ServiceBusMessage

msg = ServiceBusMessage(
    json.dumps({"payment_id": "pay-1001", "amount": 49.9}),  # body
    message_id="pay-1001", subject="payment.captured",
    content_type="application/json", correlation_id="order-1001",
    application_properties={"region": "eu", "attempt": 1,
                            "vip": True})
with shop.client() as sbc:
    with sbc.get_queue_sender("payments") as sender:
        sender.send_messages(msg)
    with sbc.get_queue_receiver("payments") as receiver:
        m = receiver.receive_messages(max_wait_time=5)[0]
        print("body:", str(m))
        print("message_id:", m.message_id, "| subject:", m.subject)
        print("content_type:", m.content_type)
        print("correlation_id:", m.correlation_id)
        print("set by the broker: sequence_number", m.sequence_number,
              "| delivery_count", m.delivery_count)
        print("raw:", m.application_properties)
        print("decoded:", shop.props(m))
        receiver.complete_message(m)
Terminal
$ python anatomy.py
body: {"payment_id": "pay-1001", "amount": 49.9}
message_id: pay-1001 | subject: payment.captured
content_type: application/json
correlation_id: order-1001
set by the broker: sequence_number 1 | delivery_count 0
raw: {b'region': b'eu', b'attempt': 1, b'vip': True}
decoded: {'region': 'eu', 'attempt': 1, 'vip': True}

Everything comes back as sent: message_id: pay-1001, subject: payment.captured, content_type: application/json and correlation_id: order-1001. The broker added the rest: sequence_number 1, unique and increasing, and delivery_count 0, zero on the first try in this Python client.

One Python detail: the raw line is {b'region': b'eu', b'attempt': 1, b'vip': True}. Keys and string values of application properties come back as bytes, while numbers and booleans keep their type. So we decode them, like the props helper does: {'region': 'eu', 'attempt': 1, 'vip': True}.

Why it matters

Filters can't read the body, so routing data belongs in properties. Properties count toward the size, with 64 KB for the whole header. And set your own business message ID.

Question 6: Your manager asks which tier to pick: Basic, Standard or Premium. How do you decide?

By features first, then by performance. Basic has queues only. Standard adds topics and subscriptions, sessions, duplicate detection and transactions. Premium has all of that on dedicated resources.

Let's go through the three tiers.

BasicStandardPremium
Queuesyesyesyes
Topics and subscriptionsnoyesyes
Sessions, duplicate detection, transactionsnoyesyes
Max message size256 KB256 KBup to 100 MB (AMQP), default 1 MB
Throughputsharedshared, variableresource isolation, predictable
Private endpoints, CMK, Geo-DR, geo-replicationnonoyes
Pricing modelper operationbase charge + operationsper messaging unit per hour

Message size is 256 KB on Basic and Standard, and up to 100 MB on Premium over AMQP. Premium also gives resource isolation and predictable performance. Private endpoints, customer-managed keys, geo-disaster recovery and geo-replication are Premium only. The pricing model differs too: Basic charges per operation, Standard adds a base charge, and Premium charges per messaging unit per hour.

In Azure, the tier is the namespace SKU, and Premium adds a capacity of 1, 2, 4, 8 or 16 messaging units.

tiers.sh
# Standard: shared capacity, base charge plus operations
az servicebus namespace create -g rg-shop -n sb-shop-std \
  --sku Standard --location eastus
# Premium: dedicated messaging units, 1, 2, 4, 8 or 16
az servicebus namespace create -g rg-shop -n sb-shop-prem \
  --sku Premium --capacity 1 --location eastus

Start with 1 or 2 messaging units, and scale up when CPU stays above 75 percent.

My default

Standard for most data pipelines, Premium for business-critical or regulated workloads. Standard moves to Premium in place, keeping the connection string, and Basic goes to Premium by way of Standard. Availability zones work on all three tiers.

Scenario 7: The business wants every online order processed reliably by billing, shipping and email, even when one of them is down. How would you design it?

One topic, order-events, with a subscription per service. The order service publishes once. Each subscription keeps its own copy and backlog, so one broken service never blocks the others.

  • Order service - publishes once
  • order-events - topic
  • billing, shipping, email - subscriptions, each with its own copy

Let's run it with billing down. We publish orders 2001 to 2003 with python publish.py 2001 2002 2003. Shipping and email are running: python worker.py shipping and python worker.py email. Then python backlog.py peeks at each subscription. Last, billing comes back with python worker.py billing, and we check the backlog again.

publish.py
import json, sys, shop
from azure.servicebus import ServiceBusMessage

with shop.client() as sbc, \
        sbc.get_topic_sender("order-events") as sender:
    for order_id in sys.argv[1:]:          # publish once per order
        sender.send_messages(ServiceBusMessage(
            json.dumps({"order_id": order_id}), message_id=order_id,
            subject="order.placed"))
print("published to order-events:", sys.argv[1:])
worker.py
import json, sys, shop

sub = sys.argv[1]                    # billing, shipping or email
done = []
with shop.client() as sbc, sbc.get_subscription_receiver(
        "order-events", sub) as receiver:            # peek-lock
    while batch := receiver.receive_messages(max_message_count=20,
                                             max_wait_time=3):
        for msg in batch:
            order = json.loads(str(msg))
            done.append(order["order_id"])   # 1. do the work
            receiver.complete_message(msg)   # 2. then settle
print(sub, "handled:", done)
backlog.py
import shop

with shop.client() as sbc:
    for sub in ["billing", "shipping", "email"]:
        with sbc.get_subscription_receiver("order-events", sub) as r:
            msgs = r.peek_messages(max_message_count=100,
                                   sequence_number=1)
        print(f"{sub:<9} waiting:", len(msgs),
              [m.message_id for m in msgs])
Terminal
$ python publish.py 2001 2002 2003
published to order-events: ['2001', '2002', '2003']
$ python worker.py shipping
shipping handled: ['2001', '2002', '2003']
$ python worker.py email
email handled: ['2001', '2002', '2003']
$ python backlog.py
billing   waiting: 3 ['2001', '2002', '2003']
shipping  waiting: 0 []
email     waiting: 0 []
$ python worker.py billing
billing handled: ['2001', '2002', '2003']
$ python backlog.py
billing   waiting: 0 []
shipping  waiting: 0 []
email     waiting: 0 []

Shipping and email each print handled: ['2001', '2002', '2003'], completing each message only after the work. The backlog shows billing waiting: 3 ['2001', '2002', '2003'], and 0 [] for shipping and email. Nothing is lost, it just waits.

Billing comes back and handles the same three orders, and every subscription shows waiting: 0 [].

To make it production grade:

  • Peek-lock - complete after the work
  • Idempotent consumers - keyed by order ID
  • Dead-letter queue - after max delivery count (default 10)
  • Duplicate detection - message_id = order ID
  • Standard or Premium - Basic has no topics
Mind the time to live

Copies that wait longer than the TTL expire, so a backlog is only safe while it is younger than that.

Key points to remember

  • A managed message broker: producers and consumers are decoupled in time
  • Namespace holds queues (one consumer per message) and topics (a copy per subscription)
  • Service Bus for messages, Event Hubs for streams, Event Grid for reactions
  • A message = body + system + application properties; routing data in properties
  • Standard for most pipelines, Premium for isolation and private networking

The full script

This is a Python script that writes every file and runs every command above in order against the Service Bus emulator: start the emulator with the Config.json above, run pip install azure-servicebus first, and set SERVICEBUS_CONNECTION_STRING.

Show the full script that writes the files and runs every step
Full script - writes the files and runs every step against the Service Bus emulator (pip install azure-servicebus), run it with Python
# ========================================================================
# Service Bus Interview Q&A #1 - Service Bus fundamentals and architecture
# TechBrothersIT. Names and sample data are invented for teaching.
# Every step below was run on the official Azure Service Bus emulator (Docker image, version 2.0.1) with
# SQL Server 2022 and azure-servicebus 7.15.0; the outputs in the video and the blog post are the real outputs.
# To run it yourself:
#   1. Start the emulator with Microsoft's installer (github.com/Azure/azure-service-bus-emulator-installer),
#      using the Config.json this script writes first (Step 0) as the emulator's config file; it holds every
#      queue, topic, subscription, rule and setting (lock duration, max delivery count, duplicate detection...)
#      the steps depend on.
#   2. pip install azure-servicebus
#   3. Set SERVICEBUS_CONNECTION_STRING to the emulator connection string (Endpoint=sb://localhost;
#      SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;)
#      then run python ServiceBus_Interview_QA_01_Code.py.
# Run it in an empty folder and on a freshly started emulator: it creates its files (and any SQLite
# databases) there, and a second run on old files or old messages gives different output.
# Steps that restart the emulator say so in a comment: stop and start it by hand at that point.
# ========================================================================

import os, subprocess, time

def write(path, text):
    if text is None:
        if os.path.exists(path): os.remove(path)
        return
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(text.strip("\n") + "\n")

def run(cmd, expect_error=False, allow_error=False, env=None):
    print("\n$ " + cmd, flush=True)
    rc = subprocess.run(cmd, shell=True, env=dict(os.environ, **(env or {}))).returncode
    if rc != 0 and not (expect_error or allow_error):
        raise SystemExit("stopped: this command was expected to succeed")

# Step 0: the emulator config (mount it as ServiceBus_Emulator/ConfigFiles/Config.json, then start the emulator)
write('Config.json', """\
{
  "UserConfig": {
    "Namespaces": [
      {
        "Name": "sbemulatorns",
        "Queues": [
          {
            "Name": "invoices",
            "Properties": {
              "DeadLetteringOnMessageExpiration": false,
              "DefaultMessageTimeToLive": "PT1H",
              "DuplicateDetectionHistoryTimeWindow": "PT20S",
              "ForwardDeadLetteredMessagesTo": "",
              "ForwardTo": "",
              "LockDuration": "PT1M",
              "MaxDeliveryCount": 3,
              "RequiresDuplicateDetection": false,
              "RequiresSession": false
            }
          },
          {
            "Name": "orders",
            "Properties": {
              "DeadLetteringOnMessageExpiration": false,
              "DefaultMessageTimeToLive": "PT1H",
              "DuplicateDetectionHistoryTimeWindow": "PT20S",
              "ForwardDeadLetteredMessagesTo": "",
              "ForwardTo": "",
              "LockDuration": "PT1M",
              "MaxDeliveryCount": 3,
              "RequiresDuplicateDetection": false,
              "RequiresSession": false
            }
          },
          {
            "Name": "payments",
            "Properties": {
              "DeadLetteringOnMessageExpiration": false,
              "DefaultMessageTimeToLive": "PT1H",
              "DuplicateDetectionHistoryTimeWindow": "PT20S",
              "ForwardDeadLetteredMessagesTo": "",
              "ForwardTo": "",
              "LockDuration": "PT1M",
              "MaxDeliveryCount": 3,
              "RequiresDuplicateDetection": false,
              "RequiresSession": false
            }
          }
        ],
        "Topics": [
          {
            "Name": "order-events",
            "Properties": {
              "DefaultMessageTimeToLive": "PT1H",
              "DuplicateDetectionHistoryTimeWindow": "PT20S",
              "RequiresDuplicateDetection": false
            },
            "Subscriptions": [
              {
                "Name": "billing",
                "Properties": {
                  "DeadLetteringOnMessageExpiration": false,
                  "DefaultMessageTimeToLive": "PT1H",
                  "LockDuration": "PT1M",
                  "MaxDeliveryCount": 3,
                  "ForwardDeadLetteredMessagesTo": "",
                  "ForwardTo": "",
                  "RequiresSession": false
                }
              },
              {
                "Name": "shipping",
                "Properties": {
                  "DeadLetteringOnMessageExpiration": false,
                  "DefaultMessageTimeToLive": "PT1H",
                  "LockDuration": "PT1M",
                  "MaxDeliveryCount": 3,
                  "ForwardDeadLetteredMessagesTo": "",
                  "ForwardTo": "",
                  "RequiresSession": false
                }
              },
              {
                "Name": "email",
                "Properties": {
                  "DeadLetteringOnMessageExpiration": false,
                  "DefaultMessageTimeToLive": "PT1H",
                  "LockDuration": "PT1M",
                  "MaxDeliveryCount": 3,
                  "ForwardDeadLetteredMessagesTo": "",
                  "ForwardTo": "",
                  "RequiresSession": false
                }
              }
            ]
          }
        ]
      }
    ],
    "Logging": {
      "Type": "File"
    }
  }
}
""")
# Step 0: the starting files
write('shop.py', 'import os\nfrom azure.servicebus import ServiceBusClient\n\n\ndef client():\n    return ServiceBusClient.from_connection_string(\n        os.environ["SERVICEBUS_CONNECTION_STRING"])\n\n\ndef props(msg):\n    """Application properties: keys and str values arrive as bytes."""\n    raw = msg.application_properties or {}\n    return {k.decode(): v.decode() if isinstance(v, bytes) else v\n            for k, v in raw.items()}\n')

# Step 1: One message to a queue, one to a topic: two competing workers and three subscriptions
write('structure.py', 'import shop\nfrom azure.servicebus import ServiceBusMessage\n\nwith shop.client() as sbc:\n    with sbc.get_queue_sender("invoices") as sender:\n        sender.send_messages(ServiceBusMessage("invoice inv-501"))\n    with sbc.get_topic_sender("order-events") as sender:\n        sender.send_messages(ServiceBusMessage("order 1001 placed"))\n    with sbc.get_queue_receiver("invoices") as w1, \\\n            sbc.get_queue_receiver("invoices") as w2:  # two workers\n        got1 = w1.receive_messages(max_wait_time=3)  # locks it\n        got2 = w2.receive_messages(max_wait_time=3)  # same queue\n        for msg in got1: w1.complete_message(msg)\n    print("queue invoices: worker-1 got", [str(m) for m in got1])\n    print("queue invoices: worker-2 got", [str(m) for m in got2])\n    for sub in ["billing", "shipping", "email"]:  # a copy each\n        with sbc.get_subscription_receiver("order-events", sub) as r:\n            got = r.receive_messages(max_wait_time=3)\n            for msg in got: r.complete_message(msg)\n        print("subscription", sub, "got", [str(m) for m in got])\n')
run('python structure.py')

# Step 2: A direct call to a billing service that is down, then the same orders through a queue
write('checkout_direct.py', """\
import urllib.request

order_id = "1001"
try:                       # billing's HTTP API: down right now
    urllib.request.urlopen(
        f"http://localhost:8099/bill?order={order_id}", timeout=3)
    print("order", order_id, "billed")
except OSError as e:
    print("order", order_id, "FAILED at checkout:",
          type(getattr(e, "reason", e)).__name__)
""")
write('checkout.py', """\
import json, sys, shop
from azure.servicebus import ServiceBusMessage

with shop.client() as sbc, sbc.get_queue_sender("orders") as sender:
    for order_id in sys.argv[1:]:
        body = json.dumps({"order_id": order_id, "total": 49.90})
        msg = ServiceBusMessage(body, message_id=order_id,
                                content_type="application/json")
        sender.send_messages(msg)   # returns after the broker's ack
        print("order", order_id, "accepted: queued for billing")
""")
write('waiting.py', """\
import sys, shop

queue = sys.argv[1]
with shop.client() as sbc, sbc.get_queue_receiver(queue) as receiver:
    msgs = receiver.peek_messages(max_message_count=100,
                                  sequence_number=1)   # no lock
ids = [m.message_id for m in msgs]
print("waiting in", queue + ":", len(msgs), ids)
""")
write('billing.py', """\
import json, shop

with shop.client() as sbc, sbc.get_queue_receiver(
        "orders") as receiver:                     # peek-lock mode
    for msg in receiver.receive_messages(max_message_count=10,
                                         max_wait_time=3):
        order = json.loads(str(msg))
        print("billing: invoiced order", order["order_id"],
              "| seq", msg.sequence_number)
        receiver.complete_message(msg)      # done: remove it
""")
run('python checkout_direct.py')
run('python checkout.py 1001 1002 1003')
run('python waiting.py orders')
run('python billing.py')
run('python waiting.py orders')

# Step 3: Body, system properties and application properties of one payment message
write('anatomy.py', """\
import json, shop
from azure.servicebus import ServiceBusMessage

msg = ServiceBusMessage(
    json.dumps({"payment_id": "pay-1001", "amount": 49.9}),  # body
    message_id="pay-1001", subject="payment.captured",
    content_type="application/json", correlation_id="order-1001",
    application_properties={"region": "eu", "attempt": 1,
                            "vip": True})
with shop.client() as sbc:
    with sbc.get_queue_sender("payments") as sender:
        sender.send_messages(msg)
    with sbc.get_queue_receiver("payments") as receiver:
        m = receiver.receive_messages(max_wait_time=5)[0]
        print("body:", str(m))
        print("message_id:", m.message_id, "| subject:", m.subject)
        print("content_type:", m.content_type)
        print("correlation_id:", m.correlation_id)
        print("set by the broker: sequence_number", m.sequence_number,
              "| delivery_count", m.delivery_count)
        print("raw:", m.application_properties)
        print("decoded:", shop.props(m))
        receiver.complete_message(m)
""")
run('python anatomy.py')

# Step 4: One topic, three subscriptions: billing is down, shipping and email keep going, billing catches up
write('publish.py', 'import json, sys, shop\nfrom azure.servicebus import ServiceBusMessage\n\nwith shop.client() as sbc, \\\n        sbc.get_topic_sender("order-events") as sender:\n    for order_id in sys.argv[1:]:          # publish once per order\n        sender.send_messages(ServiceBusMessage(\n            json.dumps({"order_id": order_id}), message_id=order_id,\n            subject="order.placed"))\nprint("published to order-events:", sys.argv[1:])\n')
write('worker.py', """\
import json, sys, shop

sub = sys.argv[1]                    # billing, shipping or email
done = []
with shop.client() as sbc, sbc.get_subscription_receiver(
        "order-events", sub) as receiver:            # peek-lock
    while batch := receiver.receive_messages(max_message_count=20,
                                             max_wait_time=3):
        for msg in batch:
            order = json.loads(str(msg))
            done.append(order["order_id"])   # 1. do the work
            receiver.complete_message(msg)   # 2. then settle
print(sub, "handled:", done)
""")
write('backlog.py', """\
import shop

with shop.client() as sbc:
    for sub in ["billing", "shipping", "email"]:
        with sbc.get_subscription_receiver("order-events", sub) as r:
            msgs = r.peek_messages(max_message_count=100,
                                   sequence_number=1)
        print(f"{sub:<9} waiting:", len(msgs),
              [m.message_id for m in msgs])
""")
run('python publish.py 2001 2002 2003')
run('python worker.py shipping')
run('python worker.py email')
run('python backlog.py')
run('python worker.py billing')
run('python backlog.py')
Next in this series

Part 2: Queues and the Message Lifecycle

The sample data on this page is invented for teaching. Every terminal output is the real output of the command above it, run on the official Azure Service Bus emulator (2.0.1) with SQL Server and azure-servicebus 7.15.0. Features the emulator does not support are shown as code. Part of the Real Time Azure Service Bus Interview Questions and Answers series on TechBrothersIT. Features, limits and their status (GA or preview) are current as of October 2026; check the official Azure Service Bus documentation (learn.microsoft.com/azure/service-bus-messaging) for anything you quote in an interview.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.