# Migrating from JMS (/connectors/how-to/migration/from-jms)



JMS (Jakarta / Java Message Service) is a **Java API specification**, not a wire protocol.
Any conforming JMS provider can be swapped for another without touching application code —
your program calls `Session.createProducer`, `Session.createConsumer`, and so on regardless
of the underlying transport. The migration path is therefore a **ConnectionFactory swap**:
replace your current provider's factory with **Apache Qpid JMS**, which speaks AMQP 1.0 over
the wire, and point it at KubeMQ's AMQP 1.0 connector. The JMS calls you already wrote stay
exactly as they are.

This guide covers the connection-factory migration, the concept-to-pattern mapping, a
self-contained Java snippet, the security posture, and the documented gaps. For the AMQP 1.0
wire contract behind Qpid JMS — frame-level semantics, link options, message translation,
selector grammar, and settlement rules — see the
[Migrating from AMQP 1.0](/connectors/how-to/migration/from-amqp-1-0) guide and the
[AMQP 1.0 connector capabilities](/connectors/amqp/reference/capabilities) reference.

## Overview [#overview]

KubeMQ's **AMQP 1.0 connector** is the substrate. Qpid JMS connects to it over AMQP 1.0 on
port **5672** (plain / SASL) or **5671** (TLS), sharing the listener with AMQP 0.9.1.

|                      |                                                                                                                                |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Connector**        | AMQP 1.0                                                                                                                       |
| **Ports**            | 5672 (plain / SASL), 5671 (TLS)                                                                                                |
| **Canonical client** | Apache Qpid JMS **2.x** (jakarta namespace) — `org.apache.qpid:qpid-jms-client:2.x`                                            |
| **Enable default**   | Opt-in — disabled by default. Set `CONNECTORS_AMQP10_ENABLE=true` (or `connectors.amqp10.enable = true` in TOML) to turn it on |

<Callout type="info">
  **Qpid JMS 2.x vs 1.x.** The jakarta-namespace release
  (`org.apache.qpid:qpid-jms-client:2.x`, artifact classifier `jakarta`) requires
  `jakarta.jms:jakarta.jms-api:3.x&#x60;. If your codebase still targets the &#x2A;*`javax.jms`*&#x2A;
  namespace (JMS 2.0), use the &#x2A;*`1.x`** line (`org.apache.qpid:qpid-jms-client:1.x`) instead.
  The Qpid URI format, the AMQP addressing, and all JNDI properties are **identical** between
  the two lines — only the JMS API namespace differs.
</Callout>

The AMQP 1.0 connector is **disabled by default** — a stock kubemq-server does not bind the
AMQP 1.0 listener until you turn it on. Enable it with its enable variable:

<RunKubeMQ ports="[5672, 5671, 50000]" env="{ CONNECTORS_AMQP10_ENABLE: 'true' }" />

<Callout type="warn">
  The enable variable is &#x2A;*`CONNECTORS_AMQP10_ENABLE`** — the literal `10` stays attached to
  `AMQP` with no underscore. `CONNECTORS_AMQP_1_0_ENABLE` and `CONNECTORS_AMQP10ENABLE` do
  **not** bind. For Kubernetes, set `spec.amqp10.enabled: true` in the `KubemqCluster` CR.
</Callout>

## Compatibility Matrix [#compatibility-matrix]

This is the JMS column of the cross-protocol matrix in the
[migration hub](/connectors/how-to/migration), restated here so this guide stands on its own.

| Dimension                 | JMS (Qpid JMS over AMQP 1.0)                      | Notes                                                                                  |
| ------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------- |
| **Drop-in level**         | client-swap                                       | Swap the ConnectionFactory; JMS API code unchanged                                     |
| **Point-to-point queues** | ✅                                                 | `Queue` → `queues/<channel>`                                                           |
| **Pub/sub (non-durable)** | ✅                                                 | `Topic` → `events/<channel>`                                                           |
| **Durable subscriptions** | ✅¹                                                | `Topic` → `events-store/<channel>` (persistence-backed)                                |
| **Request / reply (RPC)** | ✅                                                 | `QueueRequestor` / `TemporaryQueue` → Commands / Queries via dynamic nodes             |
| **Ordering guarantee**    | ⚠️ node-local                                     | Queue ordering within a single node; no cluster-wide guarantee                         |
| **Transactions**          | ❌                                                 | `SESSION_TRANSACTED` and XA are not supported                                          |
| **Dead-letter / redrive** | ❌ no client DLQ²                                  | Poison messages are dropped after `MaxReceiveCount`; no consumable dead-letter address |
| **Selectors / filtering** | ✅ selectors (SQL92 subset)                        | `createConsumer(dest, selector)` works on Events / Events Store                        |
| **Auth model**            | PLAIN (JWT) / EXTERNAL                            | SASL PLAIN with a KubeMQ JWT as password; EXTERNAL via mTLS                            |
| **TLS / mTLS**            | ✅ 5671                                            | Active when the server `Security` block is configured                                  |
| **Top unsupported**       | JMS transactions / XA; node-local `unsubscribe()` | See [What Does NOT Migrate](#what-does-not-migrate--deviations)                        |

¹ Durable subscription is backed by the persistence engine. `session.unsubscribe()` is
node-local — see [What Does NOT Migrate](#what-does-not-migrate--deviations) for the full
explanation.

² No client-settable DLQ — see [What Does NOT Migrate](#what-does-not-migrate--deviations)
for the full explanation.

## Connection / Endpoint Migration [#connection--endpoint-migration]

As shown in the [Compatibility Matrix](#compatibility-matrix) (Drop-in level: client-swap),
the only required change is the **ConnectionFactory URL** and its class name. The JMS API
calls (`createSession`, `createProducer`, `createConsumer`, and so on) remain unchanged.

### Using JNDI properties [#using-jndi-properties]

```properties
# jndi.properties — before (ActiveMQ / Artemis example)
# java.naming.factory.initial=org.apache.activemq.artemis.jndi.ActiveMQInitialContextFactory
# connectionFactory.kubemq=tcp://old-broker:61616

# jndi.properties — after (Qpid JMS over KubeMQ AMQP 1.0)
java.naming.factory.initial=org.apache.qpid.jms.jndi.JmsInitialContextFactory
connectionfactory.kubemq=amqp://kubemq.example.com:5672

# Optional TLS endpoint
# connectionfactory.kubemq=amqps://kubemq.example.com:5671

# Declare destinations (prefix drives pattern selection)
queue.ordersQueue=queues/orders
topic.ordersTopic=events/orders
topic.auditTopic=events-store/audit
```

### Direct instantiation (no JNDI) [#direct-instantiation-no-jndi]

```java
import org.apache.qpid.jms.JmsConnectionFactory;

// Before (example from any other provider):
// ConnectionFactory cf = new ActiveMQConnectionFactory("tcp://old-broker:61616");

// After (Qpid JMS):
ConnectionFactory cf = new JmsConnectionFactory("amqp://kubemq.example.com:5672");
```

For TLS, use `"amqps://kubemq.example.com:5671"`.

### Maven / Gradle dependency [#maven--gradle-dependency]

```xml
<!-- Maven — jakarta namespace (JMS 3.x) -->
<dependency>
  <groupId>org.apache.qpid</groupId>
  <artifactId>qpid-jms-client</artifactId>
  <version>2.6.0</version>
  <classifier>jakarta</classifier>
</dependency>

<!-- javax namespace (JMS 2.0), if still on javax.jms -->
<!-- <artifactId>qpid-jms-client</artifactId><version>1.12.0</version> -->
```

```groovy
// Gradle — jakarta namespace (JMS 3.x)
implementation 'org.apache.qpid:qpid-jms-client:2.6.0:jakarta'

// javax namespace (JMS 2.0), if still on javax.jms
// implementation 'org.apache.qpid:qpid-jms-client:1.12.0'
```

## Concept & Destination Mapping [#concept--destination-mapping]

Qpid JMS resolves destination names against KubeMQ by the **address prefix** — the prefix is
stripped and the remainder becomes the channel name. JMS `Queue` / `Topic` objects carry the
full address, not just the channel name.

| JMS concept                               | Qpid JMS address                          | KubeMQ pattern | Channel name    |
| ----------------------------------------- | ----------------------------------------- | -------------- | --------------- |
| `Queue` (point-to-point)                  | `queues/orders`                           | Queues         | `orders`        |
| `Topic` (non-durable pub/sub)             | `events/notifications`                    | Events         | `notifications` |
| Durable `Topic` subscription              | `events-store/audit`                      | Events Store   | `audit`         |
| `QueueRequestor` / `TemporaryQueue` reply | `commands/status` + dynamic reply node    | Commands       | `status`        |
| Virtual Topic (ActiveMQ Classic)          | `events-store/<channel>` + group property | Events Store   | —               |

<Callout type="info">
  **JMS 2.0 shared subscriptions.** `createSharedConsumer` does **not** automatically enable
  consumer-group load balancing. The AMQP 1.0 connector activates group load-balancing through
  a wire-level AMQP link property set on the ATTACH frame — which is not expressible through the
  standard JMS API. The `createSharedConsumer(dest, groupName)` call by itself does **not** wire
  the group; to load-balance Events across a group with Qpid JMS, the link property must be set
  at the wire level.
</Callout>

**Bare address (no prefix).** Qpid JMS advertises a JMS capability hint (`queue` or `topic`)
in the AMQP ATTACH. The connector uses this to resolve `Queue("orders")` → Queues and
`Topic("orders")` → Events automatically, without requiring the prefix. If you use addresses
that lack the prefix **and** the capability hint is absent, the connector falls back to the
configured default pattern (`queues`). Prefer the **explicit prefix** so resolution is never
ambiguous.

**Selectors.** Pass the selector expression as the second argument to `createConsumer` or
`createDurableConsumer`. The connector evaluates a SQL92 subset (comparisons, `AND` / `OR` /
`NOT`, `BETWEEN`, `IN`, `LIKE`, `IS NULL`) against application properties and JMS headers
(`JMSPriority`, `JMSType`, `JMSCorrelationID`, `JMSMessageID`, `JMSTimestamp`). Selectors are
supported on **Events and Events Store links only**; a selector on a Queue (`queues/`) link
fails the ATTACH with `amqp:not-implemented`.

## Canonical Client Example [#canonical-client-example]

**Client:** Apache Qpid JMS `2.6.0` (jakarta). API symbols:
`JmsConnectionFactory`, `Connection.createSession`, `Session.AUTO_ACKNOWLEDGE`,
`Session.createQueue`, `Session.createTopic`, `Session.createProducer`,
`Session.createConsumer`, `Session.createDurableConsumer`,
`Session.createTemporaryQueue`, `MessageProducer.send`, `MessageConsumer.receive`.

```java
import jakarta.jms.*;
import org.apache.qpid.jms.JmsConnectionFactory;

public class KubeMQJmsExample {

    public static void main(String[] args) throws JMSException {

        // 1. ConnectionFactory — the only change from your previous provider
        ConnectionFactory cf = new JmsConnectionFactory(
            "amqp://kubemq.example.com:5672");

        // When authentication is enabled, pass the KubeMQ JWT as the password.
        // The username is recorded for audit only and does not affect identity.
        Connection conn = cf.createConnection("svc-orders", System.getenv("KUBEMQ_JWT"));
        conn.start();

        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

        // ── Point-to-point (Queues) ──────────────────────────────────────────
        Queue ordersQueue = session.createQueue("queues/orders");

        // Produce
        TextMessage msg = session.createTextMessage("order-payload");
        session.createProducer(ordersQueue).send(msg);

        // Consume (competing consumer — AUTO_ACKNOWLEDGE maps to AMQP ACCEPTED)
        TextMessage received = (TextMessage) session.createConsumer(ordersQueue).receive(5000);
        System.out.println("Received: " + received.getText());

        // ── Non-durable pub/sub (Events) ─────────────────────────────────────
        Topic eventsTopic = session.createTopic("events/notifications");
        session.createProducer(eventsTopic).send(
            session.createTextMessage("event-payload"));
        // Subscribers on events/notifications receive a copy (fan-out).

        // ── Durable subscription (Events Store) ──────────────────────────────
        Topic auditTopic = session.createTopic("events-store/audit");
        // Durable identity derived from (container-id, subscription-name) — survives reconnect.
        MessageConsumer durableConsumer =
            session.createDurableConsumer(auditTopic, "audit-sub");

        // ── Selector on Events Store ──────────────────────────────────────────
        // SQL92 subset evaluated in the connector before delivery.
        MessageConsumer filtered = session.createConsumer(
            session.createTopic("events-store/audit"),
            "JMSPriority > 4 AND region = 'EU'");

        // ── Request / reply (Commands) ────────────────────────────────────────
        // Use a TemporaryQueue as the reply destination; the connector backs it
        // with a dynamic AMQP node (/responses/<id>).
        Queue statusCmd = session.createQueue("commands/status");
        TemporaryQueue replyQ = session.createTemporaryQueue();
        Message req = session.createTextMessage("get-status");
        req.setJMSReplyTo(replyQ);
        req.setJMSCorrelationID("req-001");
        session.createProducer(statusCmd).send(req);

        Message reply = session.createConsumer(replyQ).receive(5000);
        System.out.println("Reply: " + ((TextMessage) reply).getText());

        conn.close();
    }
}
```

<Callout type="info">
  **Acknowledge modes and AMQP settlement.** `AUTO_ACKNOWLEDGE` → AMQP `accepted` (AckRange).
  `CLIENT_ACKNOWLEDGE` → deferred `accepted` on `msg.acknowledge()`. `SESSION_TRANSACTED` is
  **not supported** — see [What Does NOT Migrate](#what-does-not-migrate--deviations).
</Callout>

## Security [#security]

### Authentication [#authentication]

* **Opt-in connector.** The AMQP 1.0 connector is disabled by default. Set
  `CONNECTORS_AMQP10_ENABLE=true` (or `connectors.amqp10.enable = true` in TOML) to start it.
* **No auth (development).** When authentication is disabled (the server default), the
  connector accepts SASL PLAIN with any credentials and also accepts SASL ANONYMOUS. If the
  server is reachable from untrusted networks, enable authentication or firewall ports
  5672 / 5671.
* **With auth enabled.** Pass a **KubeMQ JWT as the SASL PLAIN password**:

  ```java
  Connection conn = cf.createConnection("any-username", kubemqJWT);
  ```

  The username is recorded for audit; the JWT's `ClientID` claim becomes the connection
  identity for authorization checks.

### Authorization [#authorization]

With authorization enabled, the ClientID derived from the JWT is checked against the policy
on the resolved channel at ATTACH time:

* Producers (sender links) need **Write** on the channel.
* Consumers (receiver links) need **Read** on the channel.

A denied ATTACH fails with `amqp:unauthorized-access`; the JMS client throws a
`JMSSecurityException`.

### TLS / mTLS [#tls--mtls]

* **TLS (server auth).** Change the connection URL to `amqps://kubemq.example.com:5671`. The
  Qpid JMS client performs the standard TLS handshake; trust the server certificate using a
  JVM truststore or the `transport.trustStoreLocation` / `transport.trustStorePassword` URI
  options:

  ```text
  amqps://kubemq.example.com:5671?transport.trustStoreLocation=/path/to/truststore.jks
  ```

* **mTLS (client auth).** When the server requests a client certificate, the connector
  offers SASL EXTERNAL; the certificate CN becomes the ClientID (no JWT needed):

  ```text
  amqps://kubemq.example.com:5671?transport.keyStoreLocation=/path/to/keystore.jks&transport.keyStorePassword=secret
  ```

The TLS listener on port 5671 is active only when the server `Security` block is configured.
See [Auth & Security](/connectors/reference/auth-and-security) for the shared connector security
model.

### SASL mechanisms [#sasl-mechanisms]

| Mechanism   | When offered                   | Credential                                                       |
| ----------- | ------------------------------ | ---------------------------------------------------------------- |
| `PLAIN`     | always                         | password = KubeMQ JWT (auth enabled) or anything (auth disabled) |
| `ANONYMOUS` | authentication disabled        | ClientID = sanitized container-id                                |
| `EXTERNAL`  | mTLS with verified client cert | ClientID = certificate CN                                        |

## What Does NOT Migrate / Deviations [#what-does-not-migrate--deviations]

### Not supported [#not-supported]

| JMS feature                                | Status             | Notes                                                                                                                                                                                                                                                                                                                                  |
| ------------------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`SESSION_TRANSACTED`**                   | ❌ not supported    | JMS transacted sessions rely on an AMQP transaction coordinator (`declare` / `discharge`), which the connector does not implement. Fall back to `AUTO_ACKNOWLEDGE` or `CLIENT_ACKNOWLEDGE` with idempotent message design.                                                                                                             |
| **XA transactions**                        | ❌ not supported    | JMS XA (`XASession`, `XAConnectionFactory`) require two-phase AMQP transactions. Not available.                                                                                                                                                                                                                                        |
| **Client-settable DLQ**                    | ❌ no client DLQ    | When a queue message exceeds `MaxReceiveCount` (default 1024), it is **silently dropped** — not delivered to a dead-letter address. The AMQP 1.0 connector never routes poison messages to a consumable dead-letter channel. Design for idempotency and monitor `kubemq_messages_dropped_total` (CounterVec; label `cause="dropped"`). |
| **`session.unsubscribe()` (cluster-wide)** | ⚠️ node-local only | `unsubscribe()` removes the durable Events Store subscription registration on the node where it is called. If the durable subscription is active on another cluster node, that registration is not removed. Use the REST / dashboard API to delete durable subscriptions cluster-wide.                                                 |
| **`rcv-settle-mode=second`**               | ❌ not supported    | Two-phase receiver settlement (exactly-once) is not supported. The model is at-least-once (queue / durable Events Store) or at-most-once (Events fire-hose).                                                                                                                                                                           |
| **AMQP-over-WebSocket**                    | ❌ not supported    | No WebSocket binding; use raw TCP / TLS on 5672 / 5671.                                                                                                                                                                                                                                                                                |
| **Selectors on Queues**                    | ❌ not supported    | Selector expressions on `queues/` links fail the ATTACH with `amqp:not-implemented`. Selectors work on Events and Events Store links.                                                                                                                                                                                                  |
| **JMS start-position from Qpid JMS**       | ❌ not expressible  | Qpid JMS exposes no API for the `x-opt-kubemq-start` link property — you cannot set an arbitrary Events-Store start position from JMS. Use a native durable consumer with `new-only`, or a native client (Go / .NET / Python / Rust / JS) for `first` / `last` / `sequence:` / `time:`.                                                |

### Behavioral deviations [#behavioral-deviations]

| Area                                            | Behavior                                                                                                                                                                                                                                                                                                                                          |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **RPC reply shape differs by pattern**          | A JMS request / reply over a dynamic reply node behaves differently depending on the target pattern: a `commands/` reply carries **executed / error** properties, while a `queries/` reply carries **body + metadata only**. Read the reply accordingly — do not expect executed/error flags on a Queries response.                               |
| **Pub/sub events at credit 0**                  | `events/` subscribers are fire-hose: if the consumer grants no link credit, events are dropped (`kubemq_amqp10_events_dropped_no_credit_total`). Grant credit continuously with an async consumer.                                                                                                                                                |
| **Events Store stalled credit**                 | A per-link buffer (`MaxUnsettledPerLink`, default 1024) fronts the durable subscription. If it fills while credit stays at 0, the link is detached (`amqp:resource-limit-exceeded`) and the buffered window is dropped. A durable re-attach resumes *after* the dropped window — those messages are lost. Size `MaxUnsettledPerLink` accordingly. |
| **Durable-subscription registry is node-local** | Attaching the same durable subscription (same container-id + subscription name) on two cluster nodes produces a clientID conflict in the persistence engine. One attachment should be active at a time per durable-subscription identity.                                                                                                         |
| **`JMSXGroupID` / group-sequence**              | Round-trips losslessly but provides no ordering or consumer-affinity guarantee in KubeMQ.                                                                                                                                                                                                                                                         |
| **`header.priority`**                           | Round-trips as the `amqp10.priority` tag but drives no priority scheduling.                                                                                                                                                                                                                                                                       |
| **`released` increments receive count**         | Releasing a message (rollback / `recover()`) increments the receive count, which counts toward `MaxReceiveCount`. A strict reading of AMQP would not count a release as a delivery attempt.                                                                                                                                                       |
| **`modified{undeliverable-here=true}`**         | Treated as a delivery failure (NAckRange); there is no per-consumer exclusion, so the message may be redelivered to the same consumer.                                                                                                                                                                                                            |

### What does migrate cleanly [#what-does-migrate-cleanly]

* JMS `AUTO_ACKNOWLEDGE` and `CLIENT_ACKNOWLEDGE` modes.
* `Queue` (point-to-point), `Topic` (fan-out), and durable `Topic` subscriptions.
* `TextMessage`, `BytesMessage`, `MapMessage`, and `ObjectMessage` body types (round-trip
  lossless for standard properties).
* Selectors on Events / Events Store.
* `JMSReplyTo` + `JMSCorrelationID` for request / reply over a `TemporaryQueue`.
* `JMSPriority`, `JMSTimestamp`, `JMSMessageID`, `JMSType`, and `JMSCorrelationID` headers.

## Verification Smoke Test [#verification-smoke-test]

Use the point-to-point block from the
[Canonical Client Example](#canonical-client-example) as a send / receive confirmation:

1. **Enable the AMQP 1.0 connector** on the target KubeMQ server
   (`CONNECTORS_AMQP10_ENABLE=true`) and confirm port 5672 is reachable.
2. **Produce one message** — run the snippet's "Point-to-point (Queues)" block to publish a
   `TextMessage` to `queues/orders`.
3. **Consume it** — run the `createConsumer(ordersQueue).receive(5000)` block and confirm the
   text matches.
4. **Check the dashboard** — the KubeMQ dashboard AMQP 1.0 page should show one connection,
   one sender link, one receiver link, and one transfer in / out.
5. **Confirm zero dropped messages** — `kubemq_amqp10_events_dropped_no_credit_total` and
   `kubemq_amqp10_events_store_dropped_stalled_total` should remain 0.

## See Also [#see-also]

<Cards>
  <Card title="Migration hub" href="/connectors/how-to/migration" description="Choose the right KubeMQ connector for your existing broker and the cross-protocol matrix." />

  <Card title="Migrating from ActiveMQ" href="/connectors/how-to/migration/from-activemq" description="Route an ActiveMQ workload by client type — Java/JMS via Qpid JMS, STOMP and MQTT by endpoint." />

  <Card title="Migrating from AMQP 1.0" href="/connectors/how-to/migration/from-amqp-1-0" description="Point a native AMQP 1.0 client at KubeMQ — address mapping, a Go example, RPC, and deviations." />

  <Card title="AMQP 1.0 capabilities" href="/connectors/amqp/reference/capabilities" description="Exactly what the AMQP 1.0 connector supports and what it rejects on the wire." />

  <Card title="Connector configuration" href="/configure/reference/connectors#amqp-10" description="The Connectors.Amqp10 settings, env-var table, and CRD fields." />
</Cards>
