Jakarta Messaging JMS Interview Questions and Answers

Master Jakarta Messaging and JMS with production-ready interview questions covering queues, topics, producers, consumers, message types, acknowledgement, persistence, transactions, retries, redelivery, dead-letter queues, idempotency, and senior-level best practices.


Introduction

Jakarta Messaging is the Jakarta EE specification for reliable asynchronous communication between application components.

It was previously known as the Java Message Service, or JMS.

Jakarta Messaging allows applications to communicate through a messaging provider without requiring the sender and receiver to be available at the same time.

It supports two primary messaging models:

  • Point-to-point messaging using queues
  • Publish-subscribe messaging using topics

Jakarta Messaging is commonly used for:

  • Order processing
  • Payment workflows
  • Claim processing
  • Notification delivery
  • Audit events
  • File-processing jobs
  • System integration
  • Background tasks
  • Decoupling services
  • Load leveling

A production-ready messaging design must consider:

  • Delivery guarantees
  • Acknowledgement
  • Transactions
  • Redelivery
  • Duplicate messages
  • Message ordering
  • Dead-letter queues
  • Idempotency
  • Poison messages
  • Observability
  • Back pressure
  • Security

This guide covers the 15 most frequently asked Jakarta Messaging and JMS interview questions with code examples, Mermaid diagrams, production scenarios, common mistakes, and senior-level recommendations.


Q1. What is Jakarta Messaging?

Answer

Jakarta Messaging is the standard Jakarta EE API for sending and receiving messages through a messaging provider.

It allows applications to communicate asynchronously.

Main Components

  • JMSContext
  • JMSProducer
  • JMSConsumer
  • Queue
  • Topic
  • Message
  • Message-driven consumers
  • Messaging provider

Basic Architecture

flowchart LR
    A[Producer Application] --> B[Messaging Provider]
    B --> C[Consumer Application]

The producer sends a message to a destination.

The messaging provider stores and routes the message.

The consumer receives and processes it.

Basic Producer Example

import jakarta.annotation.Resource;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.jms.JMSContext;
import jakarta.jms.Queue;

@ApplicationScoped
public class OrderMessageProducer {

    @Inject
    private JMSContext jmsContext;

    @Resource(
        lookup = "java:/jms/queue/OrderQueue"
    )
    private Queue orderQueue;

    public void sendOrderCreated(
        String orderId
    ) {

        jmsContext
            .createProducer()
            .send(
                orderQueue,
                orderId
            );
    }
}

Why Interviewers Ask This

Interviewers want to confirm that you understand asynchronous messaging and the role of a broker or messaging provider.

Production Example

An order API stores an order and sends a message so inventory, notification, and fulfillment processing can continue asynchronously.

Common Mistake

Describing Jakarta Messaging as a database queue implementation.

Senior Follow-up

Jakarta Messaging defines the programming contract, while providers implement storage, routing, persistence, clustering, and delivery behavior.


Q2. What was JMS?

Answer

JMS stands for Java Message Service.

It was the original Java EE messaging API.

The modern name is Jakarta Messaging, and modern code uses the jakarta.jms namespace.

Namespace Migration

Older code:

import javax.jms.Message;
import javax.jms.Queue;

Modern code:

import jakarta.jms.Message;
import jakarta.jms.Queue;

Evolution

flowchart LR
    A[JMS] --> B[Java EE Messaging API]
    B --> C[Transferred to Jakarta EE]
    C --> D[Jakarta Messaging]
    D --> E[jakarta.jms Namespace]

Migration Considerations

  • Update imports
  • Update dependencies
  • Verify server support
  • Verify messaging-provider compatibility
  • Test message-driven consumers
  • Review transaction integration
  • Validate serialization compatibility

Common Mistake

Assuming only source imports need to change.

Senior Follow-up

Migration should verify broker clients, runtime configuration, message formats, transactions, and third-party libraries.


Q3. What is the difference between a Queue and a Topic?

Answer

A Queue supports point-to-point messaging.

A Topic supports publish-subscribe messaging.

Queue

Each message is consumed by one eligible consumer.

flowchart LR
    A[Producer] --> B[Order Queue]
    B --> C[Consumer 1]
    B -. Another message .-> D[Consumer 2]

Topic

Each eligible subscriber can receive a copy of the published message.

flowchart LR
    A[Publisher] --> B[Order Topic]
    B --> C[Inventory Subscriber]
    B --> D[Notification Subscriber]
    B --> E[Analytics Subscriber]

Comparison

Queue Topic
Point-to-point Publish-subscribe
One consumer per message Multiple subscribers may receive message
Competing consumers Independent subscribers
Work distribution Event broadcasting
Good for jobs Good for domain events

Queue Use Cases

  • File-processing jobs
  • Payment work items
  • Notification tasks
  • Report generation
  • Batch jobs

Topic Use Cases

  • Order-created event
  • Customer-updated event
  • Fraud-alert event
  • Configuration-change event
  • Audit event

Common Mistake

Using a Topic when only one worker should process each message.

Senior Follow-up

Choose the destination model according to business semantics, not only technical convenience.


Q4. What is point-to-point messaging?

Answer

Point-to-point messaging uses a Queue.

A producer sends messages to the Queue, and one consumer processes each message.

Multiple consumers can compete for messages, allowing horizontal scaling.

Flow

sequenceDiagram
    participant Producer
    participant Queue
    participant ConsumerA
    participant ConsumerB
    Producer->>Queue: Message 1
    Producer->>Queue: Message 2
    Queue->>ConsumerA: Message 1
    Queue->>ConsumerB: Message 2

Important Characteristics

  • One successful consumer per message
  • Consumers may compete
  • Messages may remain stored until consumed
  • Useful for load distribution
  • Consumer count can scale independently

Example

jmsContext
    .createProducer()
    .send(
        processingQueue,
        "JOB-1001"
    );

Consumer

JMSConsumer consumer =
    jmsContext.createConsumer(
        processingQueue
    );

String jobId =
    consumer.receiveBody(
        String.class
    );

Production Example

A file-processing Queue distributes uploaded files among ten worker instances.

Common Mistake

Assuming one specific consumer always receives a particular message.

Senior Follow-up

If message ordering matters, competing consumers and redelivery behavior must be carefully designed.


Q5. What is publish-subscribe messaging?

Answer

Publish-subscribe messaging uses a Topic.

A producer publishes an event, and multiple subscribers can receive independent copies.

Flow

sequenceDiagram
    participant Publisher
    participant Topic
    participant Inventory
    participant Notification
    participant Analytics
    Publisher->>Topic: OrderCreated
    Topic->>Inventory: OrderCreated copy
    Topic->>Notification: OrderCreated copy
    Topic->>Analytics: OrderCreated copy

Good Use Cases

  • Domain event distribution
  • Audit notifications
  • Cache invalidation
  • Analytics events
  • Multi-channel notification
  • Independent downstream workflows

Publisher Example

jmsContext
    .createProducer()
    .send(
        orderEventTopic,
        orderCreatedEvent
    );

Durable Subscription

A durable subscription allows messages to remain available for a subscriber while it is temporarily disconnected.

Concept

flowchart TD
    A[Subscriber Offline] --> B[Publisher Sends Event]
    B --> C[Broker Stores Event for Durable Subscription]
    C --> D[Subscriber Reconnects]
    D --> E[Stored Event Delivered]

Common Mistake

Assuming every Topic subscriber automatically receives messages sent while it is offline.

Senior Follow-up

Durable subscription identity, lifecycle, cleanup, and broker storage must be managed deliberately.


Q6. What is a JMS Producer?

Answer

A JMS Producer creates and sends messages to a Queue or Topic.

In the simplified Jakarta Messaging API, a producer is represented by JMSProducer.

Example

@ApplicationScoped
public class PaymentEventProducer {

    @Inject
    private JMSContext jmsContext;

    @Resource(
        lookup = "java:/jms/topic/PaymentTopic"
    )
    private Topic paymentTopic;

    public void publish(
        PaymentCompletedEvent event
    ) {

        jmsContext
            .createProducer()
            .setProperty(
                "eventType",
                "PAYMENT_COMPLETED"
            )
            .send(
                paymentTopic,
                event
            );
    }
}

Producer Responsibilities

  • Choose destination
  • Create message
  • Add properties
  • Configure delivery mode
  • Configure priority
  • Configure expiration
  • Send message

Producer Flow

flowchart TD
    A[Business Service] --> B[Create Event]
    B --> C[JMS Producer]
    C --> D[Set Headers and Properties]
    D --> E[Send to Destination]
    E --> F[Messaging Provider]

Example Properties

jmsContext
    .createProducer()
    .setPriority(7)
    .setTimeToLive(60_000)
    .setProperty(
        "tenantId",
        tenantId
    )
    .send(
        destination,
        messageBody
    );

Common Mistake

Sending large business objects without defining a stable message contract.

Senior Follow-up

Message schemas should be versioned and independent of internal entity classes.


Q7. What is a JMS Consumer?

Answer

A JMS Consumer receives messages from a Queue or Topic.

Consumers may receive messages:

  • Synchronously
  • Asynchronously
  • Through message-driven components

Synchronous Consumer

try (
    JMSConsumer consumer =
        jmsContext.createConsumer(
            orderQueue
        )
) {

    OrderMessage message =
        consumer.receiveBody(
            OrderMessage.class,
            5_000
        );
}

Asynchronous Consumer

JMSConsumer consumer =
    jmsContext.createConsumer(
        orderQueue
    );

consumer.setMessageListener(
    message -> {

        try {
            String orderId =
                message.getBody(
                    String.class
                );

            orderService.process(
                orderId
            );
        } catch (JMSException exception) {
            throw new RuntimeException(
                exception
            );
        }
    }
);

Message-Driven Consumer Concept

flowchart LR
    A[Queue] --> B[Container-Managed Message Listener]
    B --> C[Application Service]
    C --> D[Database]

Consumer Responsibilities

  • Receive message
  • Validate schema
  • Process idempotently
  • Handle failure
  • Acknowledge or roll back
  • Record observability data

Common Mistake

Performing long or unsafe processing without timeout, retry, or failure handling.

Senior Follow-up

A consumer should assume messages may be delivered more than once.


Q8. What is a JMS Message?

Answer

A JMS Message is the object transmitted through the messaging provider.

It contains:

  • Headers
  • Properties
  • Body

Message Structure

flowchart TD
    A[JMS Message] --> B[Headers]
    A --> C[Properties]
    A --> D[Body]

    B --> E[Message ID]
    B --> F[Timestamp]
    B --> G[Correlation ID]
    B --> H[Expiration]
    B --> I[Priority]

    C --> J[Business Metadata]
    D --> K[Payload]

Common Headers

  • JMSMessageID
  • JMSTimestamp
  • JMSCorrelationID
  • JMSReplyTo
  • JMSDestination
  • JMSDeliveryMode
  • JMSExpiration
  • JMSPriority
  • JMSRedelivered

Message Properties

message.setStringProperty(
    "eventType",
    "ORDER_CREATED"
);

message.setStringProperty(
    "tenantId",
    tenantId
);

Correlation ID

A correlation ID links related request and response messages.

message.setJMSCorrelationID(
    requestId
);

Common Mistake

Putting all routing information inside the message body.

Senior Follow-up

Use headers and properties for transport metadata, and keep the body focused on the business contract.


Q9. What message types are supported?

Answer

Jakarta Messaging supports several message body types.

Common Types

  • TextMessage
  • BytesMessage
  • MapMessage
  • ObjectMessage
  • StreamMessage
  • Generic Message

Comparison

Type Body
TextMessage String text
BytesMessage Raw bytes
MapMessage Key-value pairs
ObjectMessage Serializable Java object
StreamMessage Ordered primitive values
Message Headers and properties only

Text Message

TextMessage message =
    jmsContext.createTextMessage(
        """
        {
          "orderId": 101,
          "status": "CREATED"
        }
        """
    );

Map Message

MapMessage message =
    jmsContext.createMapMessage();

message.setLong(
    "orderId",
    101L
);

message.setString(
    "status",
    "CREATED"
);

Production Recommendation

Structured text formats such as JSON are often preferred for interoperability and schema evolution.

Message-Type Decision

flowchart TD
    A[Choose Message Body] --> B{Cross-language consumers?}
    B -->|Yes| C[JSON or standardized binary format]
    B -->|No| D{Need Java serialization?}
    D -->|Yes| E[ObjectMessage with caution]
    D -->|No| F[Text or Map Message]

Common Mistake

Using ObjectMessage with internal entities.

Senior Follow-up

Java serialization tightly couples producer and consumer classes and can create security, compatibility, and deployment problems.


Q10. What is message acknowledgement?

Answer

Acknowledgement confirms that a message was successfully received or processed.

The acknowledgement model determines when the broker may consider a message complete.

Common Modes

  • Automatic acknowledgement
  • Client acknowledgement
  • Duplicates-allowed acknowledgement
  • Transaction-based acknowledgement

Conceptual Flow

sequenceDiagram
    participant Broker
    participant Consumer
    participant Database
    Broker->>Consumer: Deliver message
    Consumer->>Database: Process business operation
    alt Successful
        Consumer->>Broker: Acknowledge
        Broker->>Broker: Remove or mark complete
    else Failed
        Consumer-->>Broker: No acknowledgement or rollback
        Broker->>Consumer: Redeliver later
    end

Client Acknowledgement

JMSContext context =
    connectionFactory.createContext(
        JMSContext.CLIENT_ACKNOWLEDGE
    );

JMSConsumer consumer =
    context.createConsumer(queue);

Message message =
    consumer.receive();

process(message);

message.acknowledge();

Important Nuance

Client acknowledgement may acknowledge more than one delivered message depending on session behavior.

Common Mistake

Acknowledging before business processing completes.

Senior Follow-up

Acknowledgement timing should align with the point at which the business operation is durably complete.


Q11. What is message persistence?

Answer

Message persistence determines whether the messaging provider must retain a message reliably across failures.

Delivery Modes

  • Persistent
  • Non-persistent

Persistent Message

jmsContext
    .createProducer()
    .setDeliveryMode(
        DeliveryMode.PERSISTENT
    )
    .send(
        paymentQueue,
        paymentMessage
    );

Persistent Flow

flowchart TD
    A[Producer Sends Persistent Message] --> B[Broker Stores Message Durably]
    B --> C[Broker Confirms Send]
    C --> D[Consumer Receives]
    D --> E[Successful Acknowledgement]
    E --> F[Broker Removes Message]

Non-Persistent Flow

flowchart TD
    A[Producer Sends Non-Persistent Message] --> B[Broker Routes Message]
    B --> C{Broker Failure Before Delivery?}
    C -->|Yes| D[Message May Be Lost]
    C -->|No| E[Consumer Receives]

Trade-Offs

Persistent Non-Persistent
Higher reliability Higher throughput
More storage I/O Lower storage cost
Better for critical events Better for disposable events
Survives provider restart May not survive failure

Common Mistake

Using non-persistent delivery for payment or transaction events.

Senior Follow-up

Delivery mode should be selected according to business loss tolerance, not only throughput goals.


Q12. How do transactions work in Jakarta Messaging?

Answer

Messaging operations can participate in a transaction.

A transaction can coordinate:

  • Message receive
  • Database update
  • Message send
  • Acknowledgement

Transactional Consumer Flow

sequenceDiagram
    participant Broker
    participant Consumer
    participant Database
    Broker->>Consumer: Deliver message
    Consumer->>Database: Update business data
    Consumer->>Broker: Send follow-up event
    alt Transaction succeeds
        Consumer->>Database: Commit
        Consumer->>Broker: Commit acknowledgement and send
    else Transaction fails
        Consumer->>Database: Rollback
        Consumer->>Broker: Rollback and redeliver
    end

Jakarta Transaction Example

@ApplicationScoped
public class OrderMessageHandler {

    @Inject
    private OrderService orderService;

    @Inject
    private JMSContext jmsContext;

    @Resource(
        lookup = "java:/jms/topic/OrderEventTopic"
    )
    private Topic orderEventTopic;

    @Transactional
    public void process(
        OrderMessage message
    ) {

        orderService.updateOrder(
            message.orderId()
        );

        jmsContext
            .createProducer()
            .send(
                orderEventTopic,
                new OrderProcessedEvent(
                    message.orderId()
                )
            );
    }
}

Transaction Benefits

  • Message acknowledgement follows successful processing
  • Failed work can trigger redelivery
  • Database and messaging work may be coordinated
  • Partial completion is reduced

Common Mistake

Calling an external REST API inside a long messaging transaction.

Senior Follow-up

Distributed transaction support varies. Many systems prefer transactional outbox and idempotent consumers instead of broad distributed transactions.


Q13. What is a Dead Letter Queue?

Answer

A Dead Letter Queue, or DLQ, stores messages that cannot be processed successfully after configured delivery attempts.

DLQ Flow

flowchart TD
    A[Message Delivered] --> B[Consumer Processing]
    B --> C{Success?}
    C -->|Yes| D[Acknowledge]
    C -->|No| E[Redelivery Attempt]
    E --> F{Maximum Attempts Reached?}
    F -->|No| B
    F -->|Yes| G[Move to Dead Letter Queue]
    G --> H[Alert and Investigation]

Reasons Messages Reach a DLQ

  • Invalid schema
  • Missing required data
  • Repeated downstream failure
  • Unsupported event version
  • Business-rule rejection
  • Poison message
  • Consumer bug

DLQ Processing Requirements

  • Capture failure reason
  • Preserve original message
  • Preserve message ID
  • Preserve correlation ID
  • Record retry count
  • Provide operational alerts
  • Support replay after correction
  • Prevent uncontrolled automatic replay

Example DLQ Record

{
  "originalMessageId": "ID:12345",
  "eventType": "ORDER_CREATED",
  "failureReason": "Unknown product code",
  "deliveryCount": 5,
  "failedAt": "2026-07-12T10:00:00Z"
}

Common Mistake

Treating the DLQ as permanent message storage without monitoring or ownership.

Senior Follow-up

Every DLQ requires an operational process, alerting, replay policy, and clear ownership.


Q14. How do retries and redelivery work?

Answer

When message processing fails, the messaging provider may redeliver the message.

The retry policy may include:

  • Maximum delivery count
  • Fixed delay
  • Exponential backoff
  • Destination-specific configuration
  • DLQ routing
  • Exception-based retry decisions

Redelivery Flow

sequenceDiagram
    participant Broker
    participant Consumer
    participant Dependency
    Broker->>Consumer: Delivery attempt 1
    Consumer->>Dependency: Call service
    Dependency-->>Consumer: Temporary failure
    Consumer-->>Broker: Rollback
    Broker->>Consumer: Delivery attempt 2 after delay
    Consumer->>Dependency: Call service
    Dependency-->>Consumer: Success
    Consumer->>Broker: Commit

Redelivery Indicator

boolean redelivered =
    message.getJMSRedelivered();

A provider may also expose delivery count through properties.

Retry Classification

Retriable Failures

  • Temporary database outage
  • Network timeout
  • Dependency unavailable
  • Broker interruption
  • Lock timeout

Non-Retriable Failures

  • Invalid schema
  • Missing required business field
  • Unsupported event version
  • Permanent business rejection

Retry Decision

flowchart TD
    A[Processing Failure] --> B{Temporary?}
    B -->|Yes| C[Retry with Backoff]
    B -->|No| D[Move to DLQ or Reject]
    C --> E{Retry Limit Reached?}
    E -->|No| F[Redeliver]
    E -->|Yes| D

Common Mistake

Retrying every error indefinitely.

Senior Follow-up

Retry policies must distinguish transient failures from permanent poison messages.


Q15. What are senior-level Jakarta Messaging best practices?

Answer

Senior developers should design consumers for at-least-once delivery and operational failure recovery.

Best Practices

  • Use explicit message contracts.
  • Version message schemas.
  • Keep payloads small.
  • Avoid sending JPA entities.
  • Use persistent delivery for critical events.
  • Design consumers to be idempotent.
  • Configure bounded retries.
  • Separate transient and permanent failures.
  • Use dead-letter queues.
  • Add correlation and causation IDs.
  • Validate message schema before processing.
  • Keep message transactions short.
  • Avoid remote calls inside long broker transactions.
  • Monitor queue depth and consumer lag.
  • Add processing timeouts.
  • Secure destinations.
  • Protect sensitive message data.
  • Test duplicate delivery.
  • Test consumer restart behavior.
  • Document replay procedures.

Messaging Design Flow

flowchart TD
    A[Business Event] --> B[Define Stable Message Contract]
    B --> C[Choose Queue or Topic]
    C --> D[Choose Delivery Guarantee]
    D --> E[Design Idempotent Consumer]
    E --> F[Define Retry Policy]
    F --> G[Configure DLQ]
    G --> H[Add Metrics and Alerts]
    H --> I[Test Failure and Replay]

Senior Follow-up

Reliable messaging requires application-level idempotency and observability even when the broker provides durable delivery.


Jakarta Messaging Architecture

flowchart TD
    A[REST API] --> B[Application Service]
    B --> C[Database]
    B --> D[JMS Producer]
    D --> E[Messaging Provider]

    E --> F[Queue Consumer]
    E --> G[Topic Subscriber]
    E --> H[Audit Subscriber]

    F --> I[Processing Service]
    G --> J[Notification Service]
    H --> K[Audit Store]

Queue Architecture

flowchart LR
    A[Producer 1] --> C[Work Queue]
    B[Producer 2] --> C

    C --> D[Consumer 1]
    C --> E[Consumer 2]
    C --> F[Consumer 3]

Each message is handled by one successful consumer.


Topic Architecture

flowchart LR
    A[Order Service] --> B[Order Event Topic]
    B --> C[Inventory Subscriber]
    B --> D[Billing Subscriber]
    B --> E[Notification Subscriber]
    B --> F[Analytics Subscriber]

Each subscriber receives an independent event copy.


Producer Example

@ApplicationScoped
public class ClaimEventPublisher {

    private final JMSContext jmsContext;

    @Resource(
        lookup =
            "java:/jms/topic/ClaimEvents"
    )
    private Topic claimEvents;

    @Inject
    public ClaimEventPublisher(
        JMSContext jmsContext
    ) {
        this.jmsContext =
            jmsContext;
    }

    public void publish(
        ClaimCreatedEvent event
    ) {

        jmsContext
            .createProducer()
            .setDeliveryMode(
                DeliveryMode.PERSISTENT
            )
            .setProperty(
                "eventType",
                "CLAIM_CREATED"
            )
            .setProperty(
                "schemaVersion",
                "1"
            )
            .send(
                claimEvents,
                event
            );
    }
}

Consumer Example

@ApplicationScoped
public class ClaimEventConsumer {

    private final ClaimProcessingService
        claimProcessingService;

    @Inject
    public ClaimEventConsumer(
        ClaimProcessingService
            claimProcessingService
    ) {
        this.claimProcessingService =
            claimProcessingService;
    }

    @Transactional
    public void handle(
        ClaimCreatedEvent event
    ) {

        claimProcessingService
            .processClaim(
                event.claimId()
            );
    }
}

Message-Driven Bean Example

import jakarta.ejb.ActivationConfigProperty;
import jakarta.ejb.MessageDriven;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;

@MessageDriven(
    activationConfig = {
        @ActivationConfigProperty(
            propertyName =
                "destinationLookup",
            propertyValue =
                "java:/jms/queue/OrderQueue"
        ),
        @ActivationConfigProperty(
            propertyName =
                "destinationType",
            propertyValue =
                "jakarta.jms.Queue"
        )
    }
)
public class OrderMessageListener
    implements MessageListener {

    @Override
    public void onMessage(
        Message message
    ) {

        try {
            String orderId =
                message.getBody(
                    String.class
                );

            process(orderId);

        } catch (JMSException exception) {
            throw new RuntimeException(
                exception
            );
        }
    }

    private void process(
        String orderId
    ) {
        // Business processing
    }
}

Message Contract Example

public record OrderCreatedEvent(
    String eventId,
    int schemaVersion,
    Instant occurredAt,
    Long orderId,
    Long customerId,
    BigDecimal total
) implements Serializable {
}
  • Event ID
  • Schema version
  • Occurrence timestamp
  • Correlation ID
  • Causation ID
  • Tenant ID
  • Producer name
  • Business identifier

Message Contract Design

flowchart TD
    A[Message Contract] --> B[Metadata]
    A --> C[Business Payload]

    B --> D[Event ID]
    B --> E[Schema Version]
    B --> F[Correlation ID]
    B --> G[Timestamp]

    C --> H[Business Identifier]
    C --> I[Required Event Data]

Idempotent Consumer Pattern

Because messages may be delivered more than once, consumers should prevent duplicate business effects.

Example

@Transactional
public void process(
    OrderCreatedEvent event
) {

    boolean alreadyProcessed =
        processedEventRepository
            .existsByEventId(
                event.eventId()
            );

    if (alreadyProcessed) {
        return;
    }

    orderWorkflow.start(
        event.orderId()
    );

    processedEventRepository.save(
        new ProcessedEvent(
            event.eventId(),
            Instant.now()
        )
    );
}

Idempotency Flow

flowchart TD
    A[Receive Message] --> B[Read Event ID]
    B --> C{Already Processed?}
    C -->|Yes| D[Skip Duplicate]
    C -->|No| E[Execute Business Operation]
    E --> F[Record Event ID]
    F --> G[Commit]

Important

The business operation and processed-event record should commit atomically where possible.


At-Most-Once vs At-Least-Once vs Exactly-Once

Delivery Model Meaning
At-most-once Message may be lost but not repeated
At-least-once Message is retried but may be duplicated
Exactly-once effect Business effect occurs once through coordination and idempotency

Practical Reality

flowchart TD
    A[Broker Delivers Message] --> B[Consumer Completes Business Work]
    B --> C{Acknowledgement Reaches Broker?}
    C -->|Yes| D[Complete]
    C -->|No| E[Broker Redelivers]
    E --> F[Consumer Must Detect Duplicate]

Exactly-once business behavior usually requires more than a broker guarantee.


Transactional Outbox Pattern

The outbox pattern prevents database updates and event publication from becoming inconsistent.

Problem

flowchart TD
    A[Update Database] --> B[Commit]
    B --> C[Publish Message]
    C --> D{Publish Fails?}
    D -->|Yes| E[Database Changed but Event Missing]

Outbox Solution

flowchart TD
    A[Begin Database Transaction] --> B[Update Business Table]
    B --> C[Insert Outbox Event]
    C --> D[Commit Together]
    D --> E[Outbox Publisher Reads Event]
    E --> F[Send to Broker]
    F --> G[Mark Event Published]

Example Tables

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    status VARCHAR(30) NOT NULL
);

CREATE TABLE outbox_events (
    event_id VARCHAR(100) PRIMARY KEY,
    aggregate_id BIGINT NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    payload TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL,
    published_at TIMESTAMP NULL
);

Request-Reply Messaging

JMS supports request-reply patterns using:

  • JMSReplyTo
  • JMSCorrelationID

Flow

sequenceDiagram
    participant Requester
    participant RequestQueue
    participant Responder
    participant ReplyQueue
    Requester->>RequestQueue: Request with ReplyTo and CorrelationID
    RequestQueue->>Responder: Deliver request
    Responder->>ReplyQueue: Response with same CorrelationID
    ReplyQueue->>Requester: Deliver response

Warning

Request-reply messaging creates temporal coupling and should not replace simpler synchronous APIs without a clear reason.


Message Selectors

Selectors filter messages using message headers and properties.

Example

JMSConsumer consumer =
    jmsContext.createConsumer(
        paymentQueue,
        "region = 'US' AND priorityLevel >= 5"
    );

Selector Flow

flowchart TD
    A[Queue Messages] --> B[Selector Expression]
    B --> C[Matching Messages]
    B --> D[Non-Matching Messages Remain]

Good Uses

  • Region routing
  • Message type routing
  • Priority class
  • Tenant partitioning

Risks

  • Broker-side scanning cost
  • Complex operational behavior
  • Hidden routing rules
  • Incorrect selector syntax

Message Priority

JMS priority ranges conceptually from lower to higher priority.

jmsContext
    .createProducer()
    .setPriority(9)
    .send(
        urgentQueue,
        event
    );

Warning

Priority is not always a strict ordering guarantee.

Use separate destinations for strongly separated service classes when needed.


Message Expiration

Time-to-live prevents stale messages from being delivered indefinitely.

jmsContext
    .createProducer()
    .setTimeToLive(
        300_000
    )
    .send(
        notificationQueue,
        message
    );

Expiration Flow

flowchart TD
    A[Message Sent] --> B[TTL Starts]
    B --> C{Consumed Before Expiry?}
    C -->|Yes| D[Process Message]
    C -->|No| E[Message Expires]

Good Use Cases

  • Time-sensitive notifications
  • Temporary cache invalidation
  • Short-lived status updates

Do not use expiration for events that must never be lost.


Message Ordering

Ordering can be affected by:

  • Multiple producers
  • Multiple consumers
  • Redelivery
  • Transactions
  • Priorities
  • Partitioning
  • Broker implementation

Ordering Risk

sequenceDiagram
    participant Producer
    participant Queue
    participant ConsumerA
    participant ConsumerB
    Producer->>Queue: Event 1
    Producer->>Queue: Event 2
    Queue->>ConsumerA: Event 1
    Queue->>ConsumerB: Event 2
    ConsumerB-->>Queue: Completes first
    ConsumerA-->>Queue: Completes later

Business processing order may differ from send order.

Strategies

  • Single consumer
  • Partition by business key
  • Sequence numbers
  • Version checks
  • Idempotent state transitions
  • Reject stale events

Poison Message Handling

A poison message always fails because its content is invalid or unsupported.

Flow

flowchart TD
    A[Poison Message] --> B[Consumer Fails]
    B --> C[Broker Redelivers]
    C --> B
    C --> D{Retry Limit}
    D -->|Reached| E[Dead Letter Queue]

Best Practices

  • Limit retries
  • Capture failure details
  • Alert operations
  • Preserve original payload
  • Add manual or controlled replay
  • Fix consumer or message data before replay

Retry with Exponential Backoff

Attempt 1: immediate
Attempt 2: 5 seconds
Attempt 3: 30 seconds
Attempt 4: 2 minutes
Attempt 5: DLQ

Flow

flowchart LR
    A[Attempt 1] --> B[5 Second Delay]
    B --> C[Attempt 2]
    C --> D[30 Second Delay]
    D --> E[Attempt 3]
    E --> F[2 Minute Delay]
    F --> G[Final Attempt]
    G --> H[DLQ]

Backoff helps avoid overwhelming a failing dependency.


Queue Depth and Consumer Lag

Monitor:

  • Current queue depth
  • Oldest message age
  • Enqueue rate
  • Dequeue rate
  • Consumer count
  • Processing duration
  • Redelivery rate
  • DLQ count

Capacity Flow

flowchart TD
    A[Producer Rate] --> B{Greater than Consumer Rate?}
    B -->|Yes| C[Queue Depth Grows]
    C --> D[Consumer Lag Increases]
    D --> E[Scale Consumers or Reduce Load]
    B -->|No| F[Queue Remains Stable]

Back Pressure

Messaging absorbs bursts, but it does not provide infinite capacity.

Strategies

  • Limit producer rate
  • Scale consumers
  • Apply admission control
  • Use message expiration where appropriate
  • Pause non-critical producers
  • Split workloads into separate queues
  • Alert on message age
  • Apply circuit breakers to downstream dependencies

Messaging Security

Protect:

  • Broker connections
  • Destinations
  • Credentials
  • Message data
  • Administrative interfaces
  • Replay operations

Security Layers

flowchart TD
    A[Producer] --> B[TLS Connection]
    B --> C[Authenticated Broker Session]
    C --> D[Destination Authorization]
    D --> E[Encrypted or Protected Payload]
    E --> F[Authorized Consumer]

Best Practices

  • Use TLS.
  • Use least-privilege destination permissions.
  • Rotate credentials.
  • Avoid secrets in message properties.
  • Encrypt sensitive payload fields.
  • Audit DLQ replay.
  • Separate administrative and application accounts.

Queue vs Topic Decision Matrix

Requirement Queue Topic
One worker processes each item Yes No
Distribute jobs among workers Yes No
Notify many independent systems No Yes
Load leveling Yes Limited
Broadcast domain event No Yes
Independent subscriber retry No Yes
Work queue Yes No

Synchronous vs Asynchronous Processing

Synchronous Asynchronous
Caller waits Caller continues
Immediate response Eventual completion
Simple failure feedback Requires status tracking
Tight temporal coupling Looser temporal coupling
Lower operational complexity Requires broker operations
Suitable for fast requests Suitable for background workflows

Durable vs Non-Durable Subscription

Durable Non-Durable
Messages retained while subscriber offline Offline messages may be missed
Requires stable subscription identity Simpler lifecycle
Uses broker storage Lower storage overhead
Suitable for important events Suitable for live-only updates

Message Schema Versioning

Versioned Contract

{
  "eventId": "evt-1001",
  "schemaVersion": 2,
  "eventType": "ORDER_CREATED",
  "occurredAt": "2026-07-12T10:00:00Z",
  "payload": {
    "orderId": 101,
    "customerId": 501,
    "total": 250.00
  }
}

Evolution Best Practices

  • Add optional fields.
  • Avoid renaming existing fields.
  • Preserve old meanings.
  • Support older versions during migration.
  • Validate schema version.
  • Use compatibility testing.
  • Document deprecation.

Consumer Processing Pattern

flowchart TD
    A[Receive Message] --> B[Extract Metadata]
    B --> C[Validate Schema]
    C --> D[Check Duplicate Event]
    D --> E{Already Processed?}
    E -->|Yes| F[Acknowledge and Skip]
    E -->|No| G[Execute Business Logic]
    G --> H[Record Processing Result]
    H --> I[Commit Transaction]
    I --> J[Acknowledge Message]

Production Order Workflow

flowchart TD
    A[Order API] --> B[Create Order]
    B --> C[Store Order]
    C --> D[Publish OrderCreated]

    D --> E[Inventory Queue]
    D --> F[Notification Topic]
    D --> G[Analytics Topic]

    E --> H[Reserve Inventory]
    F --> I[Send Confirmation]
    G --> J[Update Dashboard]

Failure-Recovery Architecture

flowchart TD
    A[Main Queue] --> B[Consumer]
    B --> C{Processing Result}
    C -->|Success| D[Acknowledge]
    C -->|Transient Failure| E[Retry Queue]
    E --> F[Delay]
    F --> A
    C -->|Permanent Failure| G[Dead Letter Queue]
    G --> H[Alert]
    H --> I[Investigate and Correct]
    I --> J[Controlled Replay]
    J --> A

Common Jakarta Messaging Mistakes

  • Sending entities as messages.
  • Using ObjectMessage without compatibility planning.
  • Assuming messages are delivered exactly once.
  • Ignoring duplicate delivery.
  • Acknowledging before processing completes.
  • Retrying permanent failures.
  • Using unlimited retries.
  • Ignoring dead-letter queues.
  • Sending oversized payloads.
  • Depending on strict global ordering.
  • Missing correlation IDs.
  • Missing schema versions.
  • Calling slow external systems inside long transactions.
  • Ignoring consumer lag.
  • Treating the broker as a database.
  • Using topics when work distribution is required.
  • Using queues when every subscriber needs a copy.
  • Failing to monitor expired messages.
  • Replaying DLQ messages without correction.
  • Logging sensitive payloads.

Senior Messaging Checklist

Stable Message Contract
        │
        ▼
Correct Queue or Topic
        │
        ▼
Persistent Delivery When Required
        │
        ▼
Idempotent Consumer
        │
        ▼
Bounded Retry Policy
        │
        ▼
Dead Letter Queue
        │
        ▼
Correlation and Schema Version
        │
        ▼
Lag and Failure Monitoring
        │
        ▼
Controlled Replay

Interview Quick Revision

Concept Purpose
Jakarta Messaging Standard messaging API
JMS Former name
Queue Point-to-point destination
Topic Publish-subscribe destination
Producer Sends messages
Consumer Receives messages
Message Headers, properties, and body
Acknowledgement Confirms processing
Persistent Delivery Survives provider failure
Transaction Coordinates processing
Redelivery Retries failed message
DLQ Stores repeatedly failed message
Durable Subscription Retains topic messages offline
Selector Filters messages
Idempotency Prevents duplicate effects

Key Takeaways

  • Jakarta Messaging is the standard Jakarta EE API for asynchronous message-based communication.
  • JMS is the former name of Jakarta Messaging.
  • Queues provide point-to-point delivery where one consumer successfully handles each message.
  • Topics provide publish-subscribe delivery where multiple subscribers receive independent copies.
  • Producers create and send messages, while consumers receive and process them.
  • A message contains headers, properties, and a body.
  • Text-based and schema-driven contracts are generally more interoperable than serialized Java objects.
  • Acknowledgement should occur only after durable business processing succeeds.
  • Persistent delivery is appropriate for critical business messages that must survive broker failures.
  • Messaging transactions can coordinate receiving, database updates, acknowledgement, and follow-up sends.
  • Failed messages may be redelivered, so consumers must be idempotent.
  • Retries should distinguish temporary failures from permanent poison messages.
  • Dead Letter Queues require alerts, ownership, investigation, and controlled replay procedures.
  • Durable Topic subscriptions retain messages while subscribers are offline.
  • Message ordering can be affected by concurrency, retries, priorities, and multiple consumers.
  • Correlation IDs, event IDs, schema versions, and timestamps improve traceability and compatibility.
  • The transactional outbox pattern helps prevent database updates and event publication from becoming inconsistent.
  • Queue depth, message age, redelivery rate, processing latency, and DLQ count are critical production metrics.
  • Senior developers design messaging systems around at-least-once delivery, idempotent processing, bounded retries, stable schemas, and operational recovery.