00:00

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.
Simple analogy: JMS is like a post office. A letter is delivered to the right mailbox, the receiver signs for it, and then the post office no longer keeps it.

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.
Simple analogy: Kafka is like a newspaper archive. New editions are added to the end, nothing is thrown away when someone reads it, and each reader keeps their own bookmark so they can continue where they left off or reread last week's papers.

3. How They Work: Queue vs Log

JMS queue (message removed after it is consumed)
Producer --> [ msg3 | msg2 | msg1 ] --> Consumer A  (gets msg1)
                                   --> Consumer B  (gets msg2)
After acknowledgement, msg1 and msg2 are deleted from the queue.
Kafka partition (log is kept; each group has its own offset)
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

FeatureJMSKafka
What it isA Java API standard, implemented by brokersA complete distributed platform with its own protocol
Language supportJava (other languages through broker-specific clients)Official Java client, plus clients for most languages
ArchitectureQueues (point-to-point) and topics (publish-subscribe)Distributed, partitioned, replicated log
Message storageKept until consumed and acknowledgedKept for a set time or size, even after it is read
Replay old messagesNot supported in generalYes, by moving the consumer offset
Delivery modelBroker pushes messages to consumersConsumers pull records in batches
Who tracks progressThe broker, per messageThe consumer group, using offsets
OrderingPer queue, but parallel consumers can process out of orderGuaranteed within a partition
ThroughputModerate (thousands to tens of thousands per second)Very high (hundreds of thousands to millions per second)
ScalabilityMostly vertical; clustering depends on the brokerHorizontal: add brokers and partitions
TransactionsLocal and XA transactionsKafka transactions for exactly-once read-process-write
Message featuresSelectors, priorities, expiry, request-replyKeys, headers, compaction, stream processing
Best forEnterprise and transactional messagingEvent streaming, analytics and big data pipelines

5. Message Delivery and Ordering

  • JMS acknowledgement modes: AUTO_ACKNOWLEDGE (the session acknowledges automatically), CLIENT_ACKNOWLEDGE (your code calls message.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) and acks=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.

pom.xml
<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>artemis-jakarta-client</artifactId>
    <version>2.37.0</version>
</dependency>
JmsOrderSender.java
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");
        }
    }
}
JmsOrderReceiver.java
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.

pom.xml
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.8.0</version>
</dependency>
KafkaOrderProducer.java
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
    }
}
KafkaOrderConsumer.java
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.

OrderMessaging.java (JMS)
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
    }
}
OrderEvents.java (Kafka)
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
    }
}
application.yml
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 OrderPlaced event 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.
Online store messaging
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 WhenChoose Kafka When
You need classic queues where each task is done once and then removedMany independent consumers need the same stream of events
You need XA transactions with a database or mainframe systemYou need very high throughput or large data volumes
You rely on selectors, priorities, expiry or request-replyYou need to replay or reprocess historical messages
Your company already runs IBM MQ, Artemis or another JMS brokerYou build event-driven microservices, analytics or data pipelines
Message volume is moderate and the system is mostly JavaProducers 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.