In my current project, we are using Spring Boot and RabbitMq for some internal microservices communication.
We are currently defining Queue Properties in both services, that publish/listen to this queue. Additionally, we define the exchange only in the publisher service.
However, to make it more maintainable, I would like to find a setup/best practice to define the queue once and all relevant services can rely on it.
So far, I checked the AsyncAPI Project and considered creating an extra library to outsource the configs there.
What is the best practice here or how do you do it in your projects?
There are options, none of them are perfect.
From the standpoint of "minimal responsibility" a proper way to do this would be:
The problem arises from the fact that publisher needs to know all the routing keys regardless of how they are bound, just to form them. Another one is that sometimes the subscriber needs to know them too, if there's some logic reliant on them (which doesn't seem like a good practice tbh). And to crown it all, there's no single source of truth for the contract of sent messages, they need to be implemented separately in the consumer and subscriber. Unless you are able to generate the producer and consumer code right from the spec (be it AsyncAPI or any other), you'll have to share some string constants (routing keys, queue and exchange names) between codebases, so there are no clearly separated concerns and a vulnerability to the human factor .
Another way would be to put all the responsibility on a publisher, which could create both exchange and queues and bind them properly, thus being the only source of knowledge about message routing. Yet, subscriber must specify their queue explicitly, while adding any other subscriber to the same message "topic" or set of keys will require the publisher to add one more configuration parameter (as generally you don't want to hardcode entity names that are maintained inside the independent service, using environment variables or runtime configs instead). If you manage to teach your publisher to use a spec (AsyncAPI works well), than it's less of a pain in the ass and can be treated as a part of publisher codebase. As long as a subscriber doesn't depend on routing keys but rather derives the logic from message payload, that's the best separation of concerns you could possible achieve, but it's not minimal by any means.
That option is especially unpleasant if you can't derive the configuration from a spec and have to hardcode every entity "placeholder" through the environment vars, as handling any extra subscriber on a message channel will require a new build of the publisher (to support a previously unknown var), which is responsible for that channel. The problem with sharing contracts knowledge is not solved either here.
The first approach seems to work best when you need a pub/sub model with many different messages and when you need more than 1 programming language to support. This purely EDA approach ultimately leads to a nice architecture with only 1 exchange for every publisher, 1 queue for every subscriber and the whole logic of message exchange described explicitly by the specification, relying on the routing keys. It can be slightly improved by either total codegen from the spec (totally possible with AsyncAPI) or (the opposite way) by actually generating the spec itself from some reflections, provided by live services, which then might be used to bootstrap the whole RabbitMQ internal configuration.
The second one is less prone to human factor but can be quite verbose if you have a complex communication graph, I'd suggest only to use it when you need only a few message channels with some direct 1-to-1 bindings, preferably with the same stack (as that allows sharing contracts between services seamlessly)