Exchange
The overall naming rule is, use punctuated paths (dotted paths) when naming your RabbitMQ resources.
This patterns facilitate access control rules, and in the case of routing keys it provides a good foundation to route (a subset of) messages to the consumer as needed.
Use lower-case whenever possible. RabbitMQ is case-sensitive when it comes to named resources and routing keys, keeping names in lower-case removes one potential source of error.
Exchange naming
The principle of exchange naming is:
{vendor name}.{publishing system name {.subsystem name}}.{major version info}.{datamodel name / content description}.{exchange type}
Exchange names should be suffixed with the type of exchange, e.g. .topic
.
The different type of exchanges and their corresponding naming recommendations are:
- Topic exchange -
.topic
- Fanout exchange -
.fanout
- Header exchange -
.header
- Direct exchange -
.direct
We strongly recommend using the topic exchange since it will cover the use cases for all other exchange types.
Let's walk trough an example to illustrate the naming. In this example ABB's Edgenius platform are about to publish sensor data, as SenML, to LKAB's RabbitMQ platform:
- Vendor name = 'abb'
- Publishing system name = 'edgenius'
- DataModel name / content description = 'senml'
- Exchange type = 'topic'
In the ABB Edgenius case, following the lower-case and punctuated path pattern, we would end up with something like:
abb.edgenius.senml.topic
In the above example we only have the vendor and the platform name. It might be relevant to include the publishing component/subsystem in the name to distinguish it even more. Let's say the service publishing the SenML messages is called 'SensorPublisher'.
- Subsystem name = 'sensorpublisher'
If we add the subsystem component to the exchange name it would end up looking similar to:
abb.edgenius.sensorpublisher.senml.topic
Depending on the nature of the publishing system that could be a better identifier of the publishing system. Do we plan to add more publishers to the ABB Edgenius platform? If yes, then we probably should add the subsystem to the exchange name to separate the different publishers.
'But what if we have two or more instances of the ABB Edgenius system, all of them publishing SenML messages from sensors, but from different datasources?'
Well, since the published data are following the same schema, i.e. SenML, we should stick with the existing exchange, all instances publish to the same exchange, and use the routing key to distinguish messages from the different instances. Having the instance name or id as part of the the routing key prefix would allow us to distinguish messages from different datasources when and if needed. An example routing key could look similar to:
abb.edgenius.{instance name / identifier}.senml.{data category}
, where the placeholders for 'instance name / identifier' and .'data category' are replaced by real values.
Read more on designing routing keys below.
Let's have a look at another example for a publishing system.
In this example we have a system publishing 'event' messages whenever there is a Create, Update or Delete action in the system, to allow consumers to subscribe and act on 'CUD' events.
- Vendor name = 'lkab'
- Publishing system name = 'foo'
- Subsystem = 'bar'
- DataModel name / content description = 'event'
- Exchange type = 'topic'
The example exchange name will be:
lkab.foo.bar.event.topic
Also in this example it is important to have a well thought out routing key strategy, which allows the consumers to filter as narrowly as possible.
Versioning
It's considered best practice to use the exchange name for declaring major version info, hence any new major version should be published as a new exchange.
The publisher (API owner) is responsible for the versioning strategy and to communicate new versions, breaking changes, grace period, version retirement dates etc to the existing consumers.
- Consider using version info in your exchange name from start.
- Only major version changes should be considered as a new version, strive to keep backward compatibility with minor version and patch version changes.
- As with all message format versioning, avoid breaking changes if possible. When working with json payloads, all additive changes will be backward compatible, hence consumers will not have to immedetially adopt to those changes and the version info could remain the same.
- Breaking changes in data message format OR in the routing key design, will require a new major version to be published.
- Whenever possible the publisher should run two version in parallel during a grace period to allow consumers to adapt and test.
By conducting to this best practice your consumers can feel confident that no changes will be made to data models or routing keys on existing exchanges, and they can adapt and test changes for new versions in parallel during the grace period.
If we decorate the above naming examples with version info:
abb.edgenius.sensorpublisher.v1.senml.topic
...and for the 'event' publisher, publishing 'CUD' events:
lkab.foo.bar.v1.event.topic
Routing key design
Designing routing keys is the responsibility of the publisher.
However, a well designed routing key is beneficial both for the team responsible for routing messages, as well as performance of the consumer, and it may impact the informational security.
Think of the routing key as a filtering tool for consumers, where we aim for the consumer (existing or future) to only handle messages that are of interest for their specific task.
The principle when designing routing keys is that it should be possible to distinguish messages for a given system and a given message type in a general exchange with different types of messages (e.g. a shovel that publishes everything from one RabbitMQ server to another).
Therefore it is important to prefix the routing key with system name, and then fill in with info that can be important to filter on for consumers, separated by dots.
{publishing system name / vendor name {.publishing subsystem name}}.{category level 1}. {category level 2}.{category level 3}.{data item name / identifier}
RabbitMQ has a length limit of 255 characters for the routing key.
Imagine a few different use cases as a consumer of the data publish from ABB Edgenius (all instances included).
- Someone wants to subscribe to all the data published. E.g:
abb.edgenius.#
- Someone wants to subscribe to everything from a given instance. E.g:
abb.edgenius.{instance name / identifier}.#
- Someone want to subscribe to SenML messages published from all instances. E.g:
abb.edgenius.*.senml.#
- Someone wants to subscribe to a given data category from all instances, regardless of data model in message. E.g:
abb.edgenius.*.*.{data category}
- Someone wants to subscribe to a specific item identifier. That's not supported with this routing key setup
Another example, using lkab.foo.bar
system from above. The system published Create, Update and Delete events for it's datamodels, to allow consumers to act on those events.
When we publish this type of event, we need to consider whether there will be consumers who want / need to filter by event type, i.e. Create / Update / Delete. If so, we should include the event type in the routing key. We add the event type to our template:
{publishing system name / vendor name {.publishing subsystem name}}.
{event type}
.{ data category level 1}. {category level 2}. {category level 3}. {data item name / identifier}
For the lkab.foo.bar
system we will have something like:
lkab.foo.bar.{created | updated | deleted}.bardatamodel
for events related to the bardatamodel
and lkab.foo.bar.{created | updated | deleted}.foodatamodel
for events related to the foodatamodel
- Someone wants to subscribe to all events published. E.g:
lkab.foo.bar.#
- Someone wants to subscribe to all
deleted
events published. E.g:lkab.foo.bar.deleted.#
- Someone want to subscribe to all
bardatamodel
events. E.g:lkab.foo.bar.*.bardatamodel
- Someone wants to subscribe to
created
andupdated
events onfoodatamodel
. We'll need two bindings:lkab.foo.bar.created.foodatamodel
andlkab.foo.bar.updated.foodatamodel
- When we end up with the need for two separate bindings we'll have to consider if this is an edge case or if we should consider another order on the routing key?
Depending on the nature of the system another approach would be better suited for the event publisher. If the system also published another type of message, let's say a cmd
message we might consider having the message type in the routing key as well, and consider the order in the routing key. For messages related to the bardatamodel
:
lkab.foo.bar.bardatamodel.
{event | cmd }
.{created | update | delete |
cmd-type
}
...and for messages related to the foodatamodel
:
lkab.foo.bar.foodatamodel.
{event | cmd }
.{created | update | delete |
cmd-type
}
As with exchanges and queues you should use lower-case routing keys whenever possible. Some routing keys are dynamically created by the publishing system, hence not possible to control the case of the routing key. Ensure you document those exceptions in your api docs.