Skip to content

RabbitMQ Methodology

Not many companies give much thought to the structure they create within RabbitMQ. Typically, the focus is on ensuring that each application receives its data, without considering the flow of data in each queue. As a result, queues are often named based on the system and its function, such as whether it's an "input," "retry," or "dead-letter" queue. For example, in my case, for my CBL (Common Business Logic) application, I might create queues like cbl.queue.input, cbl.queue.retry, and cbl.queue.dlq. All the data would initially flow into the input queue, and from there, the application would determine which process handles it.

At first glance, this approach seems inefficient. The biggest issue is that it prevents parallel processing of messages, as each message is stacked behind the previous one, leading to potential bottlenecks. The lack of concurrent processing means that even if multiple entities could be handled in parallel, the application struggles to scale.

For these reasons, I prefer a different method of defining queues in RabbitMQ, one that is more efficient and allows for greater flexibility in processing. By designing queues and message flows with more thoughtful granularity and consideration for parallelization, we can greatly improve the performance and scalability of our system. Below, I will describe the approach I take and how it can optimize message handling in a RabbitMQ-based system.

Naming Conventions

Let's take a look at the singular naming conventions

Exchange

Exchange names should be descriptive of the type of data they handle. Typically, I use domain names such as customer or coupon. However, in most cases, it’s better to provide subdomain-like names, such as customer.purchase, customer.profile, or coupon.event. This naming convention makes it easier to identify the type of data being processed and adds a level of structure that enhances both scalability and maintainability. By organizing exchanges with clear, meaningful names, you can quickly determine the purpose and contents of each exchange without needing to dig into its configuration or contents.

Queue

Queue names should be based on the exchange name followed by the operation being performed. For example, if the exchange is named coupon, the queue could be named coupon.change or coupon.assign to reflect the specific operation related to coupons. This convention helps clearly define the purpose of each queue and what type of message it will process.

In cases where there are multiple consumers for the same type of data, it's a good practice to append the application name to the queue name. For instance, if you have multiple applications consuming data related to coupons, you might create queues like coupon.change.app1 or coupon.assign.app2, where app1 and app2 represent the respective consumer applications. This ensures that each consumer has its own dedicated queue while still reflecting the core data and operation being processed.

By using this approach, you maintain clarity and flexibility in managing queues, particularly as the number of consumers or operations increases.

Routing Key

Routing keys should be based on the operation being performed. For example, if the operation is to change a coupon, the routing key could be change. Similarly, for assigning a coupon, the routing key might be assign. This approach ensures that messages are routed to the correct queues based on the specific action or operation required.

By defining routing keys in this way, it becomes easier to maintain a logical flow of messages through the system, as each operation has its own dedicated routing key. It also helps in ensuring that messages are processed by the correct consumers and systems, providing greater control over message routing and reducing the risk of errors.

In cases where there are multiple operations related to the same data (e.g., change and assign), this strategy helps keep the system organized and easily extensible, as new operations can be added simply by defining new routing keys.

Topology example

flowchart LR
    IS.PosPurchase -- transaction --> customer.purchase
    IS.LoyaltyProfile -- change --> loyalty.profile
    CBL.CuponService -- urgent --> coupon.event
    CBL.CuponService -- non - urgent --> coupon.event
    DWH -- change --> customer.segmentation
    ECHO --> product-catalogue.product
    ECHO --> product-catalogue.price
    ECHO --> product-catalogue.category
    ECHO --> product-catalogue.stock

    subgraph RabbitMQ
        customer.purchase -- transaction --> custpomer.purchase.transaction.cdp
        coupon.event -- urgent --> coupon.event.urgent.cdp
        coupon.event -- non - urgent --> coupon.event.non-urgent.cdp
        customer.segmentation -- change --> customer.segmentation.cdp
        loyalty.profile -- change --> loyalty.profile.cdp
        product-catalogue.product --> product-catalogue.product.ssp
        product-catalogue.product --> product-catalogue.product.wms
        product-catalogue.price --> product-catalogue.price.ssp
        product-catalogue.category --> product-catalogue.category.ssp
        product-catalogue.stock --> product-catalogue.stock.ssp
        subgraph direct
            customer.purchase
            coupon.event
            customer.segmentation
            loyalty.profile
        end
        subgraph fanout
            product-catalogue.product
            product-catalogue.price
            product-catalogue.category
            product-catalogue.stock
        end
    end

Types of Exchanges

When designing a RabbitMQ-based system, it’s essential to understand the different types of exchanges available and how they can be used to optimize message routing and processing. By selecting the appropriate exchange type for each use case, you can ensure that messages are delivered efficiently and reliably to the correct consumers.

Direct Exchange

A direct exchange routes messages to queues based on an exact match between the routing key and the queue binding key. This means that messages are delivered to queues where the routing key exactly matches the queue binding key. Direct exchanges are ideal for scenarios where messages need to be delivered to a single consumer based on specific criteria, such as the type of data or operation being performed.

Fanout Exchange

A fanout exchange routes messages to all queues that are bound to it, regardless of the routing key. This means that messages are broadcast to all consumers bound to the exchange, ensuring each consumer receives a copy of the message. Fanout exchanges are ideal for scenarios where the same message should be processed by multiple consumers, such as broadcasting updates or notifications to multiple systems or applications.

Topic Exchange

A topic exchange routes messages to queues based on pattern matching between the routing key and the queue binding key. This allows for more flexible routing, enabling you to send messages to queues based on topics or categories. Topic exchanges are ideal when messages need to be delivered to multiple consumers based on specific criteria, such as filtering messages by topic, region, or action type.

By understanding the differences between these exchange types and how they can be used to route messages effectively, you can design a more efficient and scalable RabbitMQ-based system that meets the needs of your application.


Types of Queues

When designing a RabbitMQ-based system, it's essential to understand the different types of queues available and how they can be used to optimize message processing and delivery. By selecting the appropriate queue type for each use case, you can ensure that messages are handled efficiently and reliably by the correct consumers.

Quorum Queue

A quorum queue is a highly available and durable queue designed to ensure that messages are not lost during failures. Quorum queues replicate messages across multiple nodes in a RabbitMQ cluster, offering fault tolerance and reliability. Quorum queues are ideal for use cases where message durability and availability are critical, such as processing financial transactions or critical system updates.

Classic Queue

A classic queue is a simple and lightweight queue that provides basic message handling capabilities. Classic queues are ideal for situations where message processing is straightforward, and advanced features or durability guarantees are not required. They are well-suited for scenarios where message loss is acceptable or where messages are processed quickly and do not need to be stored long-term.

Stream Queue

A stream queue is a persistent and append-only queue that stores messages in a log-like structure. Stream queues are ideal for scenarios where messages need to be stored and processed in the order they were received, such as event streams or logs. Stream queues ensure that messages are processed in the correct order and are not lost during failures, providing durability and reliability for ordered message processing.

By understanding the differences between these queue types and how they can be used to optimize message processing and delivery, you can design a more efficient and reliable RabbitMQ-based system that meets the needs of your application.


Queue Additional Properties

Dead Letter Exchange

A dead-letter exchange (DLX) is an exchange to which messages are sent when they cannot be delivered to their intended queue. This could happen for various reasons, such as the queue being full, the message being rejected, or the message expiring. By defining a dead-letter exchange, you ensure that messages are not lost but routed to a designated exchange for further processing or handling.

Message TTL (Time-to-Live)

The message TTL (time-to-live) property defines how long a message can remain in a queue before it expires. By setting a message TTL, you ensure that messages are processed within a specific timeframe, preventing old or stale messages from accumulating in the queue. This is useful in scenarios where timely processing is essential and helps avoid potential bottlenecks caused by outdated messages.

Delivery Limit

The delivery limit property defines the maximum number of times a message can be delivered to a consumer before it is considered undeliverable. By setting a delivery limit, you can prevent messages from being repeatedly delivered to a consumer that is unable to process them. This reduces the risk of message processing errors and ensures that resources are not wasted on undeliverable messages.

Manage RabbitMQ with Terraform

Managing RabbitMQ with Terraform allows you to define and automate the provisioning, configuration, and scaling of your RabbitMQ infrastructure. With Terraform, you can ensure consistency and reproducibility across environments, making it an ideal choice for maintaining RabbitMQ deployments in both cloud and on-premises setups.

One important thing to note is that the RabbitMQ provider for Terraform is not officially maintained by Broadcom, but it is a community-supported provider that works well in practice. You can find the documentation for this provider here.

This approach is particularly suitable for on-premises solutions, as Kubernetes operators for RabbitMQ already provide similar functionality for managing RabbitMQ clusters in containerized environments.

Example Terraform Configuration

resource "rabbitmq_exchange" "coupon-event" {
  name  = "coupon.event"
  vhost = "/"
  settings {
    type        = "direct"
    durable     = true
    auto_delete = false
  }
}

resource "rabbitmq_queue" "coupon-event-non-urgent-cdp" {
  name  = "coupon.event.non-urgent.cdp"
  vhost = "/"

  settings {
    durable     = true
    auto_delete = false
    arguments = {
      "x-queue-type" = "quorum"
    }
  }
}

resource "rabbitmq_binding" "coupon-event-non-urgent" {
  source           = "${rabbitmq_exchange.coupon-event.name}"
  vhost            = "/"
  destination      = "${rabbitmq_queue.coupon-event-non-urgent-cdp.name}"
  destination_type = "queue"
  routing_key      = "non-urgent"
  depends_on = [
    rabbitmq_exchange.coupon-event,
    rabbitmq_queue.coupon-event-non-urgent-cdp
  ]
  lifecycle {
    replace_triggered_by = [rabbitmq_queue.coupon-event-non-urgent-cdp]
  }
}

resource "random_password" "password" {
  length           = 16
  special          = true
  override_special = "!#$%&*()-_=+[]{}<>:?"
}

resource "rabbitmq_user" "coupon-service" {
  name     = "coupon-service"
  password = random_password.password.result
  lifecycle {
    ignore_changes = [
      password
    ]
  }
}

resource "rabbitmq_permissions" "coupon-service" {
  user  = "${rabbitmq_user.coupon-service.name}"
  vhost = "/"

  permissions {
    configure = ""
    write     = "coupon.*"
    read      = ""
  }
}