skip to content

How does the destination property map onto Kafka vs RabbitMQ, especially once a consumer group is involved?

level: middleimportance: must knowfreq 65%

answer

  1. destination default = binding name (set it!)
  2. Kafka: destination -> topic; group -> consumer group
  3. Rabbit: destination -> topic exchange; queue = destination.group
  4. No group -> broadcast (Kafka latest / Rabbit auto-delete)
  5. group = durability + competing consumers

basics

~20 s

The destination is the logical target name. On Kafka it becomes a topic. On RabbitMQ it becomes a topic exchange; adding a consumer group creates a durable queue named destination.group bound to that exchange, so the same config means different physical objects per broker.

solid answer

~40 s

destination is set per binding via spring.cloud.stream.bindings.<name>.destination and names the logical target. The Kafka binder maps it directly to a topic. The RabbitMQ binder maps it to a Topic Exchange, and the queue is derived from the consumer group: with group=g the binder declares a durable queue <destination>.<g> bound to the exchange, so all instances in that group share the queue and compete for messages (load balancing). Without a group, Kafka assigns an anonymous consumer group per instance (each instance gets every message, starting at latest), and RabbitMQ declares an anonymous auto-delete exclusive queue per instance. So the same destination + group yields a shared Kafka consumer group vs. a shared durable Rabbit queue — the abstraction is uniform, the physical objects differ. Groups also give you durability and competing-consumer semantics on both brokers.

code

yaml · 15 lines
yaml
spring:
  cloud:
    function:
      definition: handleOrder
    stream:
      bindings:
        handleOrder-in-0:
          destination: orders   # Kafka topic 'orders' | Rabbit exchange 'orders'
          group: fulfilment     # Kafka group 'fulfilment' | Rabbit queue 'orders.fulfilment'
          consumer:
            # Kafka-only knob (ignored by Rabbit binder):
            # start-offset applies when the group has no committed offset
            start-offset: earliest
        handleOrder-out-0:
          destination: shipments

go deeper

for a junior

Should know destination names the target and that Kafka=topic, Rabbit=exchange.

for a middle

Should explain the group -> durable-queue-vs-consumer-group distinction and the no-group broadcast pitfall.

for a senior

Should cover partitioning emulation on Rabbit and start-offset/provisioning gotchas.

for a principal

Should reason about prod provisioning policy (disable auto-create), and where the uniform model breaks down operationally.

**The `destination` property** is the core mapping knob. You set it per binding: `spring.cloud.stream.bindings.<bindingName>.destination=orders`. If you omit it, the destination defaults to the **binding name** itself (e.g. `process-in-0`), which is rarely what you want — always set it explicitly. **Kafka binder mapping.** `destination` becomes a **topic** of that name. The binder can auto-create it (`spring.cloud.stream.kafka.binder.auto-create-topics=true`, default true) with configurable partition count. A **consumer group** (`spring.cloud.stream.bindings.<in>.group=g`) maps directly to a **Kafka consumer group** — Kafka's native mechanism where partitions are distributed across instances so each message is processed once per group (competing consumers). **No group** ⇒ SCS assigns a unique anonymous group id per instance, so every instance receives every message, and the default start offset is **latest** (you miss messages published while down). This is the pub/sub broadcast style. **RabbitMQ binder mapping.** RabbitMQ has no topics; it routes via **exchanges** and **queues**. `destination` becomes a **Topic Exchange** named `orders` by default. The actual message store is a **queue**: - **With group `g`:** the binder declares a **durable** queue named `<destination>.<group>` (e.g. `orders.g`) and binds it to the exchange. All instances sharing that group consume from the one queue → competing consumers / load balancing, and messages survive restarts because the queue is durable. - **Without group:** the binder declares an **anonymous, auto-delete, exclusive** queue per instance (name like `orders.anonymous.<uuid>`). Each instance gets its own copy → broadcast. The queue vanishes when the instance disconnects, so undelivered messages are lost. **The unifying concept: consumer group = durability + competing consumers.** On both brokers, giving a binding a `group` means "these instances are one logical consumer; deliver each message once across the group, and keep messages while we're down." The physical realization differs (a Kafka consumer group vs. a durable Rabbit queue) but the SCS semantics are identical — that's the abstraction's payoff. **Partitioning.** SCS also abstracts partitioning: producers set `producer.partition-key-expression`/`partition-count`; consumers set `consumer.partitioned=true` and `instance-count`/`instance-index`. Kafka maps this to native topic partitions. RabbitMQ has no native partitions, so the binder emulates them by creating multiple partitioned queues (`orders.g-0`, `orders.g-1`, …) with routing keys. Again: same config, different physical implementation. **Gotchas.** - Forgetting `group` on a consumer is the classic bug: you scale to 3 instances expecting load balancing but each processes every message (Kafka anonymous group) — or worse, lose messages across restarts (Rabbit auto-delete queue). - Kafka anonymous consumers default to `latest`; set `startOffset=earliest` if you need history. - Auto-provisioning (topics/exchanges/queues created for you) is convenient in dev but many teams disable it in prod and provision infrastructure explicitly.

  • You deploy 3 instances of a consumer with no group set. On Kafka, how many times is each message processed?
    Three times — one per instance. Without a group, each instance gets its own anonymous consumer group, so all three receive every message. Set a shared group for competing-consumer/load-balanced processing.
  • On RabbitMQ, what is the name of the queue created for destination=orders, group=billing?
    orders.billing — a durable queue bound to the orders topic exchange. All instances in the billing group consume from it.

saying these in an interview costs you the question

  • Assuming multiple instances auto-load-balance without setting a group
  • Thinking RabbitMQ has topics like Kafka — it has exchanges + queues
  • Believing destination alone guarantees message durability (durability comes from group/durable queue)

context