Difference between Kafka and JMS
When Java applications need to talk to each other without waiting, they use messaging. Two names come up again and again: JMS (Java Message Service) and Apache Kafka. Both move messages from producers to consumers, but they are built on very different ideas and suit different jobs.
This tutorial explains what each one is, how they work inside, where they differ, and when to choose which, with simple Java examples in plain Java and in Spring Boot.
1. What Is JMS?
JMS (Java Message Service) is a standard Java API, not a product. It defines common interfaces such as ConnectionFactory, Queue, Topic, MessageProducer and MessageConsumer. Message brokers such as Apache ActiveMQ Artemis, IBM MQ and TIBCO EMS implement this API, so the same Java code can work with different brokers. Since version 3.0 the API is called Jakarta Messaging and lives in the jakarta.jms package.
JMS supports two messaging styles:
- Point-to-point (Queue): each message is delivered to exactly one consumer. Several consumers can share the work.
- Publish-subscribe (Topic): each message is delivered to every active subscriber.
Key characteristics:
- Reliable delivery: consumers acknowledge each message, and persistent messages survive a broker restart.
- Messages are removed after consumption: once a queue message is acknowledged, it is gone.
- Transactions: sending and receiving can be grouped into one atomic unit, and some brokers support XA transactions with databases.
- Rich features per message: selectors (filtering), priorities, expiry times and request-reply.
- Typical use: enterprise applications where every message matters, such as orders, payments and banking transactions.
2. What Is Kafka?
Apache Kafka is an open-source, distributed event streaming platform. Producers write messages (called records or events) to topics. Each topic is split into partitions, and each partition is an ordered, append-only log stored on disk and copied to several brokers. Learn more in the Apache Kafka Introduction.
Key characteristics:
- High throughput: handles hundreds of thousands to millions of messages per second by writing sequentially to disk and batching.
- Messages are kept after consumption: records stay for a configured time (for example 7 days) or size, no matter who has read them.
- Replay: consumers track their own position (the offset) and can go back to re-read old data.
- Horizontal scaling: add brokers and partitions to handle more data.
- Ordering per partition: records with the same key always go to the same partition and stay in order.
- Typical use: real-time analytics, event-driven microservices, log collection, change data capture and stream processing.
3. How They Work: Queue vs Log
Producer --> [ msg3 | msg2 | msg1 ] --> Consumer A (gets msg1)
--> Consumer B (gets msg2)
After acknowledgement, msg1 and msg2 are deleted from the queue.
Partition 0: [0] [1] [2] [3] [4] [5] [6] <-- producer appends here
^ ^
| |
Billing group (offset 2)
|
Analytics group (offset 5)
Both groups read the same records independently. Nothing is deleted on read.
The biggest difference is who remembers what was read. In JMS, the broker tracks each message and removes it after delivery. In Kafka, each consumer group tracks its own offset, and the data stays in the log. See Kafka Offset and Kafka Consumer Group.
4. Key Differences Between Kafka and JMS
| Feature | JMS | Kafka |
|---|---|---|
| What it is | A Java API standard, implemented by brokers | A complete distributed platform with its own protocol |
| Language support | Java (other languages through broker-specific clients) | Official Java client, plus clients for most languages |
| Architecture | Queues (point-to-point) and topics (publish-subscribe) | Distributed, partitioned, replicated log |
| Message storage | Kept until consumed and acknowledged | Kept for a set time or size, even after it is read |
| Replay old messages | Not supported in general | Yes, by moving the consumer offset |
| Delivery model | Broker pushes messages to consumers | Consumers pull records in batches |
| Who tracks progress | The broker, per message | The consumer group, using offsets |
| Ordering | Per queue, but parallel consumers can process out of order | Guaranteed within a partition |
| Throughput | Moderate (thousands to tens of thousands per second) | Very high (hundreds of thousands to millions per second) |
| Scalability | Mostly vertical; clustering depends on the broker | Horizontal: add brokers and partitions |
| Transactions | Local and XA transactions | Kafka transactions for exactly-once read-process-write |
| Message features | Selectors, priorities, expiry, request-reply | Keys, headers, compaction, stream processing |
| Best for | Enterprise and transactional messaging | Event streaming, analytics and big data pipelines |
5. Message Delivery and Ordering
- JMS acknowledgement modes:
AUTO_ACKNOWLEDGE(the session acknowledges automatically),CLIENT_ACKNOWLEDGE(your code callsmessage.acknowledge()),DUPS_OK_ACKNOWLEDGE(faster, duplicates possible) and transacted sessions (commit or roll back). - Kafka producer acknowledgements:
acks=0(do not wait),acks=1(leader saved it) andacks=all(all in-sync replicas saved it). See Kafka Acknowledgment. - Kafka consumer commits: consumers commit their offset after processing; if they crash before committing, the records are read again (at-least-once).
- Exactly-once: JMS can get close with transactions; Kafka offers idempotent producers and transactions for exactly-once processing between Kafka topics.
- Ordering: in Kafka, use the same message key (for example the order ID) so related events go to the same partition and stay in order.
6. JMS Example in Java
This example uses the Jakarta Messaging 3 API with an ActiveMQ Artemis broker. The JMSContext API (the "simplified API") needs far less code than the classic connection and session objects.
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jakarta-client</artifactId>
<version>2.37.0</version>
</dependency>
import jakarta.jms.ConnectionFactory;
import jakarta.jms.JMSContext;
import jakarta.jms.Queue;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
public class JmsOrderSender {
public static void main(String[] args) {
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
// try-with-resources closes the context (connection + session) automatically
try (JMSContext context = factory.createContext()) {
Queue queue = context.createQueue("OrderQueue");
context.createProducer()
.setProperty("priority", "HIGH") // custom property for selectors
.send(queue, "New Order: 12345");
System.out.println("Order sent to OrderQueue");
}
}
}
import jakarta.jms.ConnectionFactory;
import jakarta.jms.JMSConsumer;
import jakarta.jms.JMSContext;
import jakarta.jms.Queue;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
public class JmsOrderReceiver {
public static void main(String[] args) {
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
try (JMSContext context = factory.createContext(JMSContext.AUTO_ACKNOWLEDGE)) {
Queue queue = context.createQueue("OrderQueue");
// Message selector: only receive messages whose "priority" is HIGH
JMSConsumer consumer = context.createConsumer(queue, "priority = 'HIGH'");
// Wait up to 5 seconds for a message
String order = consumer.receiveBody(String.class, 5_000);
if (order != null) {
System.out.println("Received: " + order);
// AUTO_ACKNOWLEDGE: the broker now deletes this message from the queue
} else {
System.out.println("No message within 5 seconds");
}
}
}
}
7. Kafka Example in Java
This example uses the official kafka-clients library. See also Kafka Producer and Kafka Consumer.
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.8.0</version>
</dependency>
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
public class KafkaOrderProducer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// Wait until all in-sync replicas have the record
props.put(ProducerConfig.ACKS_CONFIG, "all");
// Retries never create duplicates
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
// Key "order-12345": all events of this order go to one partition, in order
ProducerRecord<String, String> record =
new ProducerRecord<>("orders", "order-12345", "New Order: 12345");
producer.send(record, (metadata, exception) -> {
if (exception == null) {
System.out.printf("Sent to partition %d at offset %d%n",
metadata.partition(), metadata.offset());
} else {
exception.printStackTrace();
}
});
} // close() flushes any records still waiting to be sent
}
}
import java.time.Duration;
import java.util.List;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
public class KafkaOrderConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// Each consumer group keeps its own offsets
props.put(ConsumerConfig.GROUP_ID_CONFIG, "billing-service");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// A new group starts reading from the beginning of the topic
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// Commit offsets ourselves, only after processing
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
consumer.subscribe(List.of("orders"));
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("partition=%d offset=%d key=%s value=%s%n",
record.partition(), record.offset(),
record.key(), record.value());
}
// Save progress; the records themselves stay in Kafka
consumer.commitSync();
}
}
}
}
To replay all orders, a consumer can call consumer.seekToBeginning(consumer.assignment()) after it has been assigned partitions, or start a new consumer group with auto.offset.reset=earliest. There is no equivalent in a standard JMS queue, because consumed messages are already deleted.
8. Spring Boot: JmsTemplate vs KafkaTemplate
Spring Boot makes both look similar. With spring-boot-starter-artemis you get a JmsTemplate, and with spring-kafka you get a KafkaTemplate. See Kafka with Spring Boot for a full guide.
package com.javabytechie.orders.jms;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
@Component
public class OrderMessaging {
private final JmsTemplate jmsTemplate;
public OrderMessaging(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public void sendOrder(String order) {
jmsTemplate.convertAndSend("OrderQueue", order);
}
@JmsListener(destination = "OrderQueue")
public void receiveOrder(String order) {
System.out.println("JMS received: " + order); // removed from the queue after success
}
}
package com.javabytechie.orders.kafka;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
@Component
public class OrderEvents {
private final KafkaTemplate<String, String> kafkaTemplate;
public OrderEvents(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishOrder(String orderId, String order) {
kafkaTemplate.send("orders", orderId, order); // key = orderId keeps events ordered
}
@KafkaListener(topics = "orders", groupId = "billing-service")
public void onOrder(String order) {
System.out.println("Kafka received: " + order); // record stays in the topic
}
}
spring:
artemis:
broker-url: tcp://localhost:61616
kafka:
bootstrap-servers: localhost:9092
consumer:
auto-offset-reset: earliest
producer:
acks: all
9. Real-World Example: An Online Store
Imagine an online store that needs two different kinds of messaging:
- Payment instructions (JMS): when an order is paid, the Order Service sends a "charge card" command to the Payment Service through a JMS queue. Exactly one Payment Service instance must process it, inside a transaction, and the message is no longer needed afterwards. This is classic JMS work.
- Order events (Kafka): every order also produces an
OrderPlacedevent on a Kafka topic. The Inventory, Recommendation, Analytics and Fraud Detection services each read the same stream at their own pace. Months later, a new Loyalty Service can replay past events to calculate points. This is classic Kafka work.
Order Service --(JMS queue: ChargeCard)--> Payment Service (one consumer, then deleted)
Order Service --(Kafka topic: orders)--+--> Inventory Service
+--> Analytics Service
+--> Fraud Detection
+--> Loyalty Service (added later, replays history)
10. When to Choose JMS or Kafka
| Choose JMS When | Choose Kafka When |
|---|---|
| You need classic queues where each task is done once and then removed | Many independent consumers need the same stream of events |
| You need XA transactions with a database or mainframe system | You need very high throughput or large data volumes |
| You rely on selectors, priorities, expiry or request-reply | You need to replay or reprocess historical messages |
| Your company already runs IBM MQ, Artemis or another JMS broker | You build event-driven microservices, analytics or data pipelines |
| Message volume is moderate and the system is mostly Java | Producers and consumers use many different languages |
If you are also comparing Kafka with RabbitMQ, see Kafka vs RabbitMQ.
11. Using Both Together
Many large companies use both: JMS for existing transactional systems and Kafka for event streaming and analytics. They can be connected with Kafka Connect using a JMS source or sink connector, or with an integration tool such as Apache Camel. This lets old and new systems share data without rewriting either one.
12. Common Mistakes
- Calling Kafka "a JMS implementation": Kafka does not implement the JMS API; it has its own client API and protocol.
- Using Kafka as a simple task queue when you need per-message features such as priorities, delays or individual retries; JMS brokers handle these more naturally.
- Expecting JMS to keep history: consumed queue messages are deleted, so plan for replay needs up front.
- Ignoring Kafka message keys and then being surprised that related events arrive out of order.
- Auto-committing Kafka offsets before processing, which can lose messages if the consumer crashes.
- Forgetting to close JMS contexts and Kafka clients, which leaks connections; use try-with-resources.
13. Summary
JMS is a Java messaging standard, implemented by brokers such as ActiveMQ Artemis and IBM MQ. It offers queues and topics, reliable per-message delivery, selectors and transactions, and it removes messages after they are consumed. That makes it ideal for traditional enterprise and transactional messaging. Kafka is a distributed event streaming platform built on a partitioned, replicated log. It keeps messages after they are read, lets consumers replay data, scales horizontally to very high throughput, and serves many consumers from one stream. Choose JMS for reliable, task-style messaging in Java systems, choose Kafka for high-volume event streams and analytics, and do not be afraid to use both where each fits best.
Frequently Asked Questions
1. What is the main difference between Kafka and JMS?
JMS is a Java API standard for traditional messaging with queues and topics, where messages are usually deleted after they are consumed. Kafka is a distributed event streaming platform that stores messages in a log for a set time, so many consumers can read them independently and replay them later.
2. Is Kafka a JMS implementation?
No. Kafka does not implement the JMS API. It has its own client API and network protocol. Third-party bridges and Kafka Connect connectors can move data between JMS brokers and Kafka.
3. Is JMS a message broker?
No. JMS is only a specification of Java interfaces. A JMS provider, such as Apache ActiveMQ Artemis, IBM MQ or TIBCO EMS, is the actual broker that stores and delivers messages.
4. What is Jakarta Messaging?
It is the new name of JMS since version 3.0, after Java EE moved to the Eclipse Foundation as Jakarta EE. The API is the same idea, but the package changed from javax.jms to jakarta.jms.
5. What is the difference between a JMS queue and a JMS topic?
A queue delivers each message to exactly one consumer (point-to-point). A topic delivers each message to all active subscribers (publish-subscribe).
6. How is a Kafka topic different from a JMS topic?
A JMS topic pushes each message to current subscribers and does not keep it for later readers (unless a durable subscription exists). A Kafka topic is a stored, partitioned log: any number of consumer groups can read it at any time, from any offset, while the data is retained.
7. Can Kafka replay messages? Can JMS?
Kafka can replay messages by moving a consumer's offset backwards or starting a new consumer group from the beginning. Standard JMS queues cannot replay, because consumed messages are deleted; some brokers offer non-standard features to keep copies.
8. Which one is faster?
Kafka usually delivers much higher throughput, often hundreds of thousands to millions of messages per second, thanks to sequential disk writes, batching and partitioning. JMS brokers typically handle thousands to tens of thousands per second, but can have very low latency for individual messages.
9. Which is better for transactions?
JMS has long supported local transactions and XA (two-phase commit) transactions that can include a database. Kafka supports its own transactions for exactly-once processing between Kafka topics, but not XA with external databases.
10. Does Kafka guarantee message order?
Yes, within a single partition. Records with the same key always go to the same partition, so events for one order or one customer stay in order. There is no ordering guarantee across different partitions.
11. How do JMS and Kafka consumers get messages?
JMS brokers usually push messages to listeners. Kafka consumers pull batches of records by calling poll(), which lets each consumer read at its own speed.
12. What are JMS acknowledgement modes?
- AUTO_ACKNOWLEDGE: the session acknowledges each message automatically.
- CLIENT_ACKNOWLEDGE: your code calls
message.acknowledge(). - DUPS_OK_ACKNOWLEDGE: lazy acknowledgement; faster, but duplicates are possible.
- Transacted session: messages are acknowledged when the transaction commits.
13. What is a Kafka offset?
An offset is the position number of a record in a partition. Each consumer group stores the offset it has reached, so it knows where to continue after a restart:
consumer.commitSync(); // save the current position for this consumer group
14. How long does Kafka keep messages?
As long as the topic's retention settings allow, for example 7 days (retention.ms) or a maximum size (retention.bytes). Compacted topics keep the latest value for each key indefinitely.
15. Can several services read the same message?
In Kafka, yes: each consumer group gets every record and tracks its own offset. In JMS, a queue message goes to only one consumer, while a topic message goes to all subscribers that are active (or durable) at that time.
16. Can I use Kafka from languages other than Java?
Yes. Besides the official Java client, there are widely used clients for Python, Go, C/C++, .NET, Node.js and more. JMS itself is Java-only, although many JMS brokers also offer other protocols such as AMQP or MQTT.
17. Does Kafka support message priorities or selectors like JMS?
Not built in. JMS supports message priorities and SQL-like selectors to filter messages. In Kafka, you usually create separate topics for different priorities and filter records in the consumer or with Kafka Streams.
18. How do I use JMS and Kafka in Spring Boot?
For JMS, add spring-boot-starter-artemis (or another JMS starter) and use JmsTemplate and @JmsListener. For Kafka, add spring-kafka and use KafkaTemplate and @KafkaListener. Both are configured in application.yml.
19. Can JMS and Kafka be used together?
Yes. Many companies keep JMS for existing transactional systems and add Kafka for event streaming and analytics. Kafka Connect JMS connectors or tools like Apache Camel can copy messages between them.
20. Should I migrate from JMS to Kafka?
Only if you need what Kafka offers: very high throughput, replay, many independent consumers or event streaming. If your system relies on XA transactions, selectors, priorities or simple task queues and works well, JMS may still be the better fit. Many teams migrate step by step, running both during the transition.