DEV Community

Cover image for Bounded Contexts with Azure Service Bus: Scaling Domain-Driven Design
Daniel Azevedo
Daniel Azevedo

Posted on

Bounded Contexts with Azure Service Bus: Scaling Domain-Driven Design

Hi devs,
In Domain-Driven Design (DDD), one of the most fundamental concepts is the Bounded Context. It’s a way to handle complexity by defining boundaries where specific domain models and terminologies apply. But how do these contexts communicate, especially in a distributed environment? One effective way is by using Azure Service Bus for asynchronous, event-driven messaging.

Let’s dive into what Bounded Contexts are, why they matter, and how Azure Service Bus can help keep them decoupled but connected.

What is a Bounded Context?

A Bounded Context is a logical boundary within which a specific model and domain language apply. Inside a context, terms and entities have consistent meanings, while outside, they might differ based on the needs of other areas of the business. For example:

  • In a Sales context, an "Order" might mean a customer’s purchase.
  • In an Inventory context, "Order" could refer to the restocking of products.

By defining these boundaries, we avoid ambiguity, reduce complexity, and enable teams to work independently.

Why Use Azure Service Bus for Communication?

While each Bounded Context operates independently, real-world applications often require data to flow across contexts. For instance, when an order is placed in the Sales context, the Shipping context needs to be notified to arrange delivery. Azure Service Bus is an ideal solution for this because:

  1. Asynchronous Messaging: Allows different parts of the system to communicate without being directly dependent on each other.
  2. Reliability: Azure Service Bus guarantees message delivery and supports advanced features like retry policies.
  3. Scalability: Azure Service Bus handles large volumes of messages, making it suitable for complex, distributed systems.

Implementing Bounded Contexts with Azure Service Bus

Let’s break down a scenario where we have two Bounded Contexts: Order Management and Shipping. When an order is created, we want the Shipping context to be notified so it can handle shipment preparation. We’ll use Azure Service Bus to publish an OrderPlaced event that the Shipping context will listen to.

Step 1: Define Context-Specific Models

Each context maintains its own model, ensuring that the logic and data are consistent within its boundaries.

Order Management Context:

namespace OrderManagement
{
    public class Order
    {
        public int OrderId { get; set; }
        public DateTime OrderDate { get; set; }
        public decimal TotalAmount { get; set; }
    }

    public class OrderService
    {
        private readonly IEventPublisher _eventPublisher;

        public OrderService(IEventPublisher eventPublisher)
        {
            _eventPublisher = eventPublisher;
        }

        public void PlaceOrder(Order order)
        {
            // Order placement logic here
            _eventPublisher.PublishOrderPlacedEvent(order.OrderId);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, the OrderService class handles placing an order and publishing an OrderPlaced event via the IEventPublisher interface, which we’ll implement using Azure Service Bus.

Step 2: Use Azure Service Bus for Event-Driven Messaging

EventPublisher Implementation:

using Azure.Messaging.ServiceBus;

namespace OrderManagement
{
    public class ServiceBusEventPublisher : IEventPublisher
    {
        private readonly ServiceBusClient _serviceBusClient;
        private readonly string _topicName = "orderplacedtopic";

        public ServiceBusEventPublisher(ServiceBusClient serviceBusClient)
        {
            _serviceBusClient = serviceBusClient;
        }

        public async Task PublishOrderPlacedEvent(int orderId)
        {
            var sender = _serviceBusClient.CreateSender(_topicName);

            var message = new ServiceBusMessage(orderId.ToString())
            {
                Subject = "OrderPlaced",
                ApplicationProperties = { { "OrderId", orderId } }
            };

            await sender.SendMessageAsync(message);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we use the Azure Service Bus SDK to publish an event to a topic named orderplacedtopic. When an order is placed, an OrderPlaced event is sent to the Service Bus topic, notifying any subscribers.

Step 3: Set Up the Subscriber in the Shipping Context

The Shipping context subscribes to the OrderPlaced event and processes it when received.

Shipping Context:

using Azure.Messaging.ServiceBus;

namespace Shipping
{
    public class OrderPlacedListener
    {
        private readonly ServiceBusProcessor _processor;

        public OrderPlacedListener(ServiceBusClient serviceBusClient, string topicName, string subscriptionName)
        {
            _processor = serviceBusClient.CreateProcessor(topicName, subscriptionName);

            _processor.ProcessMessageAsync += ProcessOrderPlacedMessage;
            _processor.ProcessErrorAsync += ErrorHandler;
        }

        public async Task StartProcessingAsync() => await _processor.StartProcessingAsync();

        private async Task ProcessOrderPlacedMessage(ProcessMessageEventArgs args)
        {
            string orderId = args.Message.ApplicationProperties["OrderId"].ToString();
            Console.WriteLine($"OrderPlaced event received with Order ID: {orderId}");

            // Shipment preparation logic here
            await args.CompleteMessageAsync(args.Message);
        }

        private Task ErrorHandler(ProcessErrorEventArgs args)
        {
            Console.WriteLine($"Error handling message: {args.Exception}");
            return Task.CompletedTask;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Shipping context listens to messages from the orderplacedtopic topic. When an OrderPlaced event is received, the OrderPlacedListener triggers the shipment preparation process.

Advantages of Using Bounded Contexts with Azure Service Bus

  1. Scalability: With Azure Service Bus handling inter-context communication, each context can be scaled independently, enhancing the system’s overall scalability.
  2. Loose Coupling: Bounded Contexts communicate without knowing each other’s internal details, reducing dependencies and making it easier to modify or replace one context without impacting others.
  3. Resilience: Azure Service Bus provides guaranteed message delivery and fault tolerance, ensuring that events between contexts are not lost and are processed reliably.
  4. Improved Maintainability: Isolating each context reduces complexity, making the codebase easier to maintain.

Final Thoughts

Bounded Contexts provide a strong foundation for managing complexity in large-scale systems. By combining this concept with Azure Service Bus, we can create a robust, scalable, and decoupled system architecture. With Bounded Contexts, each part of the application remains focused on its responsibilities while Azure Service Bus seamlessly connects these contexts, allowing them to work together without direct dependencies.

Keep Coding

Top comments (0)