Where RabbitMQ fits
Between services that must not block on each other. A charger reports a session stop; the session service records it and publishes an event; billing, notifications, partner-facing OCPI push, and analytics all consume independently. If the analytics consumer is down for an hour, sessions keep closing and invoices keep generating — that isolation is the entire value proposition.
Topology that scales with the platform
- Topic exchanges with disciplined routing keys:
session.stopped.cpo-xstyle keys let consumers bind precisely — adding a consumer never touches producers. - Queue per consumer service, not per event type: each service owns its queue, its prefetch, and its scaling.
- Dead-letter exchanges everywhere: a poison message parks for inspection instead of blocking the queue — with alerting on DLQ depth, because a growing DLQ is an incident in progress.
- Quorum queues for the money path: replicated queues for CDR and billing events; classic queues where loss is tolerable.
Consumer rules that prevent 2 a.m. calls
- Idempotency is mandatory: every consumer handles redelivery — dedupe on message ID or natural business key before side effects.
- Manual acks after successful processing, never auto-ack — an exception after auto-ack is a silently lost event.
- Bounded retries with backoff, then DLQ — infinite retry on a malformed payload is a self-inflicted outage.
- Prefetch tuned per workload: 1 for heavy CDR processing, 50+ for lightweight notifications.
Worked example: topology and a safe consumer
Topic exchange, quorum queue on the money path, dead-letter parking, and manual acks — declared in code so environments never drift:
await channel.ExchangeDeclareAsync("platform.events", ExchangeType.Topic, durable: true);
await channel.QueueDeclareAsync("billing.session-events", durable: true, arguments:
new Dictionary<string, object> {
["x-queue-type"] = "quorum", // replicated: money path
["x-dead-letter-exchange"] = "platform.dlx",
["x-delivery-limit"] = 5 // then park, don't loop
});
await channel.QueueBindAsync("billing.session-events", "platform.events", "session.stopped.*");
consumer.ReceivedAsync += async (_, ea) =>
{
try
{
await HandleAsync(Deserialize(ea.Body)); // idempotent inside
await channel.BasicAckAsync(ea.DeliveryTag, false);
}
catch (TransientException)
{
await channel.BasicNackAsync(ea.DeliveryTag, false, requeue: false); // -> retry via DLX
}
};
FAQ
RabbitMQ for work distribution and routing between services — my default. Kafka when you need replayable history or stream processing at scale. Most platforms need reliable routing long before they need a distributed log.
Clustered brokers with quorum queues, publisher confirms on critical paths, and an outbox pattern so events survive service crashes between database commit and publish.
Yes — MQTT for device-to-cloud (chargers, vending machines), bridged into RabbitMQ for service-to-service distribution. Different protocols for different trust and reliability domains.