- Chapter
- Chapter 1 — Spring Integration
- Prerequisites
- Topics 1.1–1.2
- You will be able to
- Compare the major messaging systems by model and guarantees, use Spring's templates and listeners for each, abstract brokers with Spring Cloud Stream, and choose a messaging system for a given workload
- Covers
- Kafka · RabbitMQ · JMS · Cloud Messaging
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally
- 4A. Deep Dive — Broker-Neutral Services with Spring Cloud Stream
- 5. APIs / Features / Syntax
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
A messaging system (message broker or event streaming platform) stores and delivers messages between producers and consumers, decoupling them in time and space. Spring provides consistent programming models for the major systems: a template for sending (KafkaTemplate, RabbitTemplate, JmsTemplate) and annotated listeners for receiving (@KafkaListener, @RabbitListener, @JmsListener), plus Spring Cloud Stream as a broker-neutral abstraction.

2Why It Exists
Direct synchronous calls couple availability and throughput of services. Brokers absorb load spikes, let consumers be offline, enable fan-out to many consumers and provide delivery guarantees. Different workloads — high-volume event streams, task queues with priorities, enterprise transactional messaging, serverless pipelines — led to different broker designs, and Spring supports all of them with similar APIs.

3Core Concepts
3.1 Kafka
| Aspect | Kafka |
|---|---|
| Model | Topics split into partitions (append-only logs), consumers track offsets |
| Ordering | Per partition (per key) |
| Retention | Time/size based or compacted — replay possible |
| Scaling | Partitions × consumer groups |
| Spring | KafkaTemplate, @KafkaListener (Chapter 2) |
| Typical use | Event streaming, event-driven microservices, CDC, analytics pipelines |
3.2 RabbitMQ

@Configuration
class RabbitConfig {
@Bean TopicExchange orders() { return new TopicExchange("orders"); }
@Bean Queue billing() {
return QueueBuilder.durable("billing").quorum()
.deadLetterExchange("orders.dlx").build();
}
@Bean Binding billingBinding() {
return BindingBuilder.bind(billing()).to(orders()).with("order.*.created");
}
}
@Component
class BillingListener {
@RabbitListener(queues = "billing")
void on(OrderCreated event) { billingService.invoice(event); }
}
rabbitTemplate.convertAndSend("orders", "order.eu.created", new OrderCreated(orderId));| Aspect | RabbitMQ |
|---|---|
| Model | Exchanges (direct, topic, fanout, headers) route to queues |
| Delivery | Per-message acknowledgements, redelivery, DLX, TTL, priorities |
| Queue types | Classic, quorum (replicated, recommended), streams (log-like) |
| Spring | Spring AMQP: RabbitTemplate, @RabbitListener |
| Typical use | Task/work queues, command messaging, complex routing, RPC |
3.3 JMS
@JmsListener(destination = "payments.requests", concurrency = "3-10")
public void onPayment(PaymentRequest request) { payments.process(request); }
jmsTemplate.convertAndSend("payments.requests", request);| Aspect | JMS (Jakarta Messaging) |
|---|---|
| Nature | A Java API standard, implemented by brokers (ActiveMQ Artemis, IBM MQ, …) |
| Model | Queues (point-to-point) and topics (pub-sub), durable subscriptions |
| Transactions | Local transacted sessions; XA with JTA (legacy) |
| Spring | JmsTemplate, @JmsListener, DefaultMessageListenerContainer |
| Typical use | Enterprise and mainframe integration, existing MQ estates |
3.4 Cloud messaging
| Service | Model | Spring integration |
|---|---|---|
| AWS SQS / SNS | Queues / pub-sub fan-out | Spring Cloud AWS (SqsTemplate, @SqsListener) |
| AWS Kinesis / MSK | Streams / managed Kafka | Kinesis binder / Spring Kafka |
| Azure Service Bus / Event Hubs | Broker with sessions/DLQ / streaming (Kafka-compatible) | Spring Cloud Azure |
| Google Pub/Sub | Global pub-sub | Spring Cloud GCP (PubSubTemplate) |
4How It Works Internally
Common Spring messaging mechanics:
1. Templates convert objects to broker messages via MessageConverters (JSON typically), set headers/properties and send (synchronously or returning futures).
2. Listener containers own consumer connections and threads: they poll or receive pushes, convert messages, invoke the annotated method, and acknowledge/commit on success.
3. Error handling in containers retries, rejects/requeues or dead-letters failed messages according to configuration.
4. Transactions: JMS/AMQP containers can use local transactions; Kafka has its own transactional producer model (Chapter 2).
5. Spring Cloud Stream binds functional beans (Function, Consumer, Supplier) to destinations through binder implementations, hiding broker APIs.
4ADeep Dive — Broker-Neutral Services with Spring Cloud Stream
@SpringBootApplication
class ShippingApplication {
@Bean
Function<OrderPaid, ShipmentRequested> ship(ShippingService shipping) {
return shipping::requestShipment; // pure business function
}
}spring:
cloud:
function:
definition: ship
stream:
bindings:
ship-in-0:
destination: orders.paid
group: shipping # consumer group / durable queue
ship-out-0:
destination: shipments.requested
kafka: # or rabbit: … (binder-specific tuning)
binder:
brokers: kafka:9092| Abstraction level | Pros | Cons |
|---|---|---|
| Native clients (Kafka/AMQP APIs) | Full control | Boilerplate, broker lock-in in code |
| Spring templates + listeners | Idiomatic, full broker features | Broker-specific code |
| Spring Cloud Stream | Portable business code, consistent conventions | Lowest common denominator; broker features via binder config |
| Spring Integration | Rich EIP flows over any transport | Complexity for simple cases |
5APIs / Features / Syntax
| System | Send | Receive | Boot starter |
|---|---|---|---|
| Kafka | KafkaTemplate | @KafkaListener | spring-boot-starter-kafka |
| RabbitMQ | RabbitTemplate | @RabbitListener | spring-boot-starter-amqp |
| JMS | JmsTemplate, JmsClient (Spring 7) | @JmsListener | spring-boot-starter-artemis / provider starter |
| Cloud Stream | StreamBridge, Supplier | Consumer/Function beans | spring-cloud-stream-binder-* |
| AWS SQS | SqsTemplate | @SqsListener | Spring Cloud AWS starters |
6Practical Examples
6.1 Same event, three brokers
kafkaTemplate.send("orders.created", order.id().toString(), event); // key → partition
// exchange + routing key
rabbitTemplate.convertAndSend("orders", "order.eu.created", event);
jmsTemplate.convertAndSend("ORDERS.CREATED", event); // queue/topic6.2 Choosing a messaging system

6.3 Guarantees compared
| Property | Kafka | RabbitMQ (quorum queues) | JMS broker | SQS standard / FIFO |
|---|---|---|---|---|
| Ordering | Per partition | Per queue (single consumer) | Per queue (single consumer) | None / per message group |
| Delivery | At-least-once (EOS within Kafka) | At-least-once | At-least-once / transacted | At-least-once / exactly-once dedup window |
| Replay | Yes | No (streams: yes) | No | No |
| Routing | Topic/partition only | Rich (exchanges) | Selectors | Filtering via SNS |
6.4 Operational comparison
| Aspect | Self-managed | Managed |
|---|---|---|
| Kafka | Brokers, KRaft, upgrades, storage | MSK, Confluent Cloud, Event Hubs (Kafka API) |
| RabbitMQ | Cluster, quorum queues, upgrades | Amazon MQ, CloudAMQP |
| JMS | MQ servers, HA pairs | Cloud MQ offerings |
| Cloud-native | — | SQS/SNS, Service Bus, Pub/Sub |
6.5 Testing messaging code
Use Testcontainers (KafkaContainer, RabbitMQContainer, Artemis images) with @ServiceConnection to run listeners against real brokers in integration tests (Chapter 5), and embedded Kafka (@EmbeddedKafka) for lightweight Kafka tests.
7Common Problems & Mistakes
- Choosing Kafka for task queues needing per-message routing and priorities (or RabbitMQ for event replay).
- Assuming exactly-once delivery end to end.
- No dead-letter handling.
- Ignoring ordering requirements when scaling consumers.
- Huge messages instead of claim checks.
- Broker as database (unbounded retention used as system of record without design).
Each of these mistakes has a recognisable production signature. Using Kafka as a task queue shows up as head-of-line blocking: one slow or poison record stalls its whole partition, because offsets are committed in order and there is no per-message requeue, so a DefaultErrorHandler with a DeadLetterPublishingRecoverer becomes mandatory. Assuming exactly-once delivery leads to duplicate payments or e-mails after a rebalance or redelivery, which idempotent consumers keyed on a message id would have absorbed. Missing dead-letter handling turns one malformed message into an endless retry loop that burns CPU and hides everything queued behind it; RabbitMQ quorum queues help by enforcing a delivery limit. Scaling consumers breaks ordering silently: raising concurrency on a RabbitMQ queue or a JMS listener lets messages for the same order be processed in parallel, so updates arrive out of sequence. Oversized messages hit broker limits, around 1 MB by default in Kafka, and slow down replication, which is why the claim-check pattern stores the payload in object storage and sends only a reference.
8Performance & Production Considerations
| Concern | Guidance |
|---|---|
| Throughput | Kafka for very high volume; batching and compression |
| Latency | RabbitMQ low per-message latency; Kafka tuned via linger/batch |
| Durability | Replication (Kafka RF=3, quorum queues), persistent messages |
| Monitoring | Consumer lag, queue depth, redeliveries, DLQ size |
9Architecture & Design Considerations
- Events vs commands: topics for facts, queues for work.
- One broker technology per purpose, not per team.
- Schema management (JSON Schema, Avro, Protobuf + registry).
- Outbox pattern to publish reliably from database transactions.
10Interview Questions & Answers
What is the main difference between Kafka and RabbitMQ?
Kafka is a distributed, partitioned commit log, while RabbitMQ is a message broker built around queues. In Kafka, producers append records to topic partitions, records are retained by time or size whether or not anyone has read them, and each consumer group tracks its own offset, so consumers can replay history and many groups can read the same data independently. Ordering is guaranteed per partition, which in practice means per key. In RabbitMQ, producers publish to exchanges, which route messages to queues by binding rules; consumers acknowledge each message, after which it is deleted, and failures can be redelivered or dead-lettered. Kafka suits event streams and replay, RabbitMQ suits work distribution and flexible routing.
What is JMS?
JMS, now Jakarta Messaging 3.1, is a Java API standard rather than a broker. It defines how Java code connects, sends and receives through a ConnectionFactory, sessions, producers and consumers, with two destination types: queues for point-to-point delivery and topics for publish-subscribe, including durable subscriptions. Brokers such as ActiveMQ Artemis and IBM MQ implement the API, but their wire protocols differ, so JMS gives code portability, not interoperability between brokers. It supports local transacted sessions and, historically, XA transactions through JTA. Spring wraps it with JmsTemplate, the new fluent JmsClient in Framework 7, and @JmsListener with DefaultMessageListenerContainer. It remains common in banking and mainframe integration with existing MQ estates.
How do you send and receive messages with Spring for RabbitMQ?
With Spring AMQP and Boot's spring-boot-starter-amqp, you send through RabbitTemplate, typically convertAndSend(exchange, routingKey, payload), which converts the object with the configured MessageConverter, usually a JSON converter, and publishes it to the exchange. You receive by annotating a bean method with @RabbitListener(queues = "billing"); a listener container consumes from the queue, converts the body to the method's parameter type, invokes the method and acknowledges on success. Topology such as a TopicExchange, a durable quorum Queue and a Binding is declared as beans, and RabbitAdmin creates it at start-up. For failures, configure retries and a dead-letter exchange, and enable publisher confirms when the producer must know that the broker accepted the message.
What is Spring Cloud Stream?
Spring Cloud Stream is a framework for event-driven microservices that binds plain functional beans to messaging destinations. You write a Supplier, Function or Consumer containing business logic, and the framework, through a binder for Kafka, RabbitMQ or a cloud service, connects its inputs and outputs to topics or exchanges named in configuration, such as ship-in-0 bound to orders.paid with a consumer group. It handles serialisation, consumer groups, partitioning, retries and dead-letter destinations consistently, and StreamBridge sends messages imperatively when needed. The benefit is uniform conventions and portable business code; the limit is that broker-specific behaviour still leaks through, so it does not remove the need to choose a broker deliberately.
When would you choose RabbitMQ over Kafka?
I choose RabbitMQ when the workload is distributing work rather than publishing a replayable stream of facts. Typical signs are task or command queues where each message is processed once and then discarded, per-message acknowledgement and redelivery so that one slow message does not block others, priorities, per-message or per-queue TTLs, delayed retries, and routing by topic patterns or headers through exchanges. RabbitMQ also offers low latency at moderate volume and supports request-reply patterns. Kafka wins when consumers need replay, several independent consumer groups, long retention, very high throughput or strict per-key ordering at scale. When both needs exist, many organisations run Kafka for events and RabbitMQ or SQS for work queues.
What delivery guarantee should you design for?
Design for at-least-once delivery with idempotent consumers. Every mainstream broker redelivers a message if the consumer crashes before acknowledging or committing, and producers retry when an acknowledgement is lost, so duplicates are normal rather than exceptional. At-most-once loses data, and exactly-once holds only inside specific boundaries: Kafka transactions give exactly-once for read-process-write within Kafka, and SQS FIFO queues deduplicate within a five-minute window. As soon as the consumer writes to a database or calls an external API, those guarantees end. So consumers record processed message ids or use upserts and natural keys, and producers use the outbox pattern so the database change and the event cannot diverge.
What are the benefits of managed cloud messaging?
Managed services such as SQS and SNS, Amazon MSK, Azure Service Bus or Google Pub/Sub remove broker operations: no patching, cluster sizing, disk management or failover drills. They scale elastically, integrate with cloud identity, encryption and monitoring, and often bill per use, which suits spiky or small workloads. The costs are semantics and limits that differ from self-managed brokers, such as SQS standard queues having no ordering, message-size and throughput quotas, visibility-timeout behaviour, and provider-specific APIs that tie code to one cloud. Bills can also grow sharply at high volume. I accept those costs when operations capacity is the bottleneck, and keep application code behind Spring templates or Spring Cloud Stream to contain lock-in.
How would you define a messaging strategy for an organisation?
I would start from purposes rather than products: event streaming for facts that many consumers need and may replay, and work queues for commands that are processed once. Pick one technology per purpose, often Kafka or a managed Kafka for events and RabbitMQ or a cloud queue for work, and publish platform conventions for topic and queue naming, partitioning keys, schemas in a registry with compatibility rules, security, retries and dead-letter handling. Mandate the outbox pattern for publishing from database transactions, and idempotent consumers. Provide an internal Spring starter that configures templates, listeners, tracing and consumer-lag metrics consistently. Finally, govern new broker requests through ADRs so the estate does not fragment team by team.