Spring Boot: Other Messaging Options

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Kafka and RabbitMQ cover this section’s two main brokers in depth. This page rounds out the rest of what a Spring Boot 4.1.x application (Spring Framework 7.0.x) reaches for when a message needs to move somewhere — in-process, over JMS, through Spring Integration, over WebSocket, or to Apache Pulsar — and says, briefly, what is deliberately left out.

In-process events: ApplicationEventPublisher and @EventListener

Before reaching for a broker at all, a Spring application already has a messaging mechanism built into the ApplicationContext: publish an event with ApplicationEventPublisher.publishEvent(Object), and any bean with a matching @EventListener method receives it, synchronously, on the publishing thread, by default. This is the right tool for decoupling two parts of the same process — an order service that should not know which other components care that an order was placed — without any of it leaving the JVM.

public record OrderPlacedEvent(String orderId) {
}

@Service
public class OrderService {

    private final ApplicationEventPublisher publisher;

    public OrderService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    @Transactional
    public void placeOrder(Order order) {
        orderRepository.save(order);
        publisher.publishEvent(new OrderPlacedEvent(order.id()));
    }
}

A plain @EventListener fires while placeOrder is still inside its transaction, which is often not what a listener that sends a notification or calls another service wants — it would run even if the transaction later rolled back. @TransactionalEventListener binds the listener to a TransactionPhase instead: BEFORE_COMMIT, AFTER_COMMIT (the default), AFTER_ROLLBACK, or AFTER_COMPLETION for either outcome. Combined with @Async, the listener also moves off the caller’s thread, so placeOrder returns without waiting for it:

@Component
public class OrderPlacedNotifier {

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderPlaced(OrderPlacedEvent event) {
        notificationClient.notifyOrderPlaced(event.orderId());
    }
}

@Async here requires @EnableAsync and a configured TaskExecutor, exactly as for any other asynchronous method. See Spring Framework Documentation — Standard and Custom Events for ApplicationEventPublisher, @EventListener and @Async listeners, and Spring Framework Documentation — Transaction-bound Events for @TransactionalEventListener and TransactionPhase.

Spring Modulith: externalizing application events to Kafka or RabbitMQ

Application events stop at the process boundary — a second instance of the same service, or a different service entirely, never sees them. Spring Modulith’s event externalization bridges exactly that gap: the same ApplicationEventPublisher.publishEvent() call above, annotated once, also lands on a broker topic or exchange, with no separate publishing code path to keep in sync.

An event opts in with @Externalized, whose value is a routing target and, after ::, an optional routing key — either can be a SpEL expression evaluated against the event instance:

@Externalized("orders.order-placed::#{#this.orderId()}")
public record OrderPlacedEvent(String orderId) {
}

Which broker the routing target resolves against depends purely on which Spring Modulith events artifact is on the classpath: spring-modulith-events-kafka routes it as a Kafka topic and message key (via Spring Kafka, so a KafkaTemplate still needs its usual connection properties), and spring-modulith-events-amqp routes it as an AMQP exchange/routing key (via Spring AMQP, with an explicit spring-rabbit dependency still required). Both depend on spring-modulith-events-api, which also exposes EventExternalizationConfiguration for programmatic control — selecting which events externalize, mapping an event to a different payload, or adding headers — when annotating every event class is not the right fit:

@Configuration
public class EventExternalizationConfig {

    @Bean
    EventExternalizationConfiguration eventExternalizationConfiguration() {
        return EventExternalizationConfiguration.externalizing()
                .select(EventExternalizationConfiguration.annotatedAsExternalized())
                .routeKey(OrderPlacedEvent.class, OrderPlacedEvent::orderId)
                .build();
    }
}

This only externalizes what a listener inside the same module already reacted to in-process — it is not a replacement for Kafka or RabbitMQ producer code, it is a way to avoid hand-writing that producer code for events a module already publishes internally. See Spring Modulith Documentation — Application Events for the full @Externalized target syntax, the JMS and Spring Messaging externalization artifacts this page does not cover, and the transactional-outbox modes for relational databases — see The Transactional Outbox Pattern for the pattern itself.

JMS / Jakarta Messaging with ActiveMQ Artemis

Where Kafka and RabbitMQ (AMQP) are covered in their own sections of this site, spring-boot-starter-artemis brings in Jakarta Messaging (JMS) against Apache ActiveMQ Artemis, Spring Boot’s supported JMS broker. JmsTemplate is auto-configured from spring.jms./spring.artemis. properties — spring.artemis.mode chooses native (connect to a standalone broker via spring.artemis.broker-url) or embedded (start an in-process broker, useful for tests) — and sending is a single call:

@Component
public class OrderJmsSender {

    private final JmsTemplate jmsTemplate;

    public OrderJmsSender(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public void send(Order order) {
        jmsTemplate.convertAndSend("orders.queue", order);
    }
}

@JmsListener (enabled by the auto-configured JmsListenerContainerFactory, no @EnableJms needed in a Boot application) consumes it:

@Component
public class OrderJmsListener {

    @JmsListener(destination = "orders.queue")
    public void onOrder(Order order) {
        process(order);
    }
}

JMS destinations are queues by default; a destination becomes a topic by setting pubSubDomain to true on the listener container factory (or the JmsTemplate used to send), which is the JMS analogue of choosing point-to-point versus publish-subscribe delivery. For request/reply, JmsTemplate.convertSendAndReceive() sends and blocks for a correlated reply, and @JmsListener can answer inline with @SendTo(destination) on the handler method, letting the container populate the sender’s JMSReplyTo destination without extra wiring.

RabbitMQ ships its own JMS client (com.rabbitmq.jms, layered over AMQP 0-9-1, with a broker-side plugin needed for topic destinations) for applications that want the JMS API against a RabbitMQ broker specifically — it is a separate integration from the Artemis-based JMS support above and not interchangeable with it at the wire level, so pick one JMS provider per destination, not a mix. See Spring Boot Documentation — JMS for the spring-boot-starter-artemis auto-configuration, spring.artemis./spring.jms. properties and connection pooling, and Spring Framework Documentation — JMS (Java Message Service) for JmsTemplate, @JmsListener/@EnableJms, message conversion and convertSendAndReceive().

Spring Integration: channel adapters for AMQP and Kafka

Spring Integration turns "send this to AMQP" or "consume from this Kafka topic" into a MessageChannel endpoint in a Java DSL IntegrationFlow, so it composes with transformers, routers, splitters and aggregators instead of being one more standalone client to wire by hand. Amqp.outboundAdapter()/Amqp.inboundAdapter() and Kafka.outboundChannelAdapter()/Kafka.messageDrivenChannelAdapter() are the entry points for each broker:

@Bean
public IntegrationFlow orderEventsToRabbit(RabbitTemplate rabbitTemplate) {
    return IntegrationFlow.from("orderEvents")
            .handle(Amqp.outboundAdapter(rabbitTemplate).routingKey("orders.placed"))
            .get();
}

@Bean
public IntegrationFlow orderEventsToKafka(ProducerFactory<String, Order> producerFactory) {
    return IntegrationFlow.from("orderEvents")
            .handle(Kafka.outboundChannelAdapter(producerFactory).topic("orders"))
            .get();
}

The same families of adapters — Amqp.inboundAdapter() and Kafka.messageDrivenChannelAdapter() for consuming, plus their gateway counterparts for request/reply — are exactly what Spring Batch Integration uses to wire remote chunking and remote partitioning across a message broker; that page covers the batch-specific configuration (@EnableBatchIntegration, ChunkMessageChannelItemWriter, MessageChannelPartitionHandler) in depth and is not repeated here. See Spring Integration Documentation — AMQP Support and Spring Integration Documentation — Apache Kafka Support for the full adapter and gateway catalogue, including the pollable Kafka.inboundChannelAdapter() variant and Kafka-backed `MessageChannel`s.

WebSocket messaging with STOMP

@EnableWebSocketMessageBroker turns Spring MVC into a STOMP-over-WebSocket endpoint: clients subscribe to destinations and a @Controller handles messages sent to /app/*, exactly like an HTTP controller handles requests. Where subscriptions get broadcast is a separate choice — the *simple broker keeps subscriptions and messages in memory inside the application itself, fine for a single instance or local development:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic", "/queue");
    }
}

Swapping enableSimpleBroker for enableStompBrokerRelay hands broadcasting to an external, full STOMP broker instead — RabbitMQ, with its STOMP plugin enabled, is a common choice, since it is then the same broker already running for AMQP messaging elsewhere in the application, and it is what makes multi-instance fan-out and acks/receipts work, which the simple broker does not support:

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
    registry.setApplicationDestinationPrefixes("/app");
    registry.enableStompBrokerRelay("/topic", "/queue")
            .setRelayHost("rabbitmq")
            .setRelayPort(61613)
            .setClientLogin("app")
            .setClientPasscode("secret");
}

Server-side code publishes to either broker the same way, through an injected SimpMessagingTemplate, without knowing which one is behind it. See Spring Framework Documentation — Enabling STOMP over WebSocket for @EnableWebSocketMessageBroker and endpoint/broker registration, and Spring Framework Documentation — STOMP Broker Relay for enableStompBrokerRelay() and its RabbitMQ-relay configuration.

Apache Pulsar

spring-boot-starter-pulsar auto-configures a PulsarClient against spring.pulsar.client.service-url and, from it, a PulsarTemplate for producing and a listener container factory for @PulsarListener:

@Component
public class OrderPulsarProducer {

    private final PulsarTemplate<Order> pulsarTemplate;

    public OrderPulsarProducer(PulsarTemplate<Order> pulsarTemplate) {
        this.pulsarTemplate = pulsarTemplate;
    }

    public void send(Order order) {
        pulsarTemplate.send("orders", order);
    }
}

@Component
public class OrderPulsarListener {

    @PulsarListener(topics = "orders", subscriptionName = "orders-subscription")
    public void onOrder(Order order) {
        process(order);
    }
}

spring.pulsar.producer. and spring.pulsar.consumer./spring.pulsar.listener.* tune producer batching/caching and consumer/container behavior respectively, and spring.pulsar.admin.service-url configures the auto-configured PulsarAdministration client for topic/subscription management. @PulsarReader is the manual-cursor counterpart to @PulsarListener for applications that need to control message acknowledgement and positioning themselves rather than through a managed subscription. See Spring Boot Documentation — Apache Pulsar for the starter’s auto-configuration and property surface, and Spring for Apache Pulsar Documentation for PulsarTemplate, @PulsarListener/@PulsarReader and the underlying client/producer/consumer factories.

What this page defers

Redis pub/sub and Redis Streams are both viable Spring Boot messaging options in their own right, but they are deliberately out of scope here and covered instead under Redis Reference: Streams & Pub/Sub and, for the Spring Data Redis repository/template side, Spring Data Redis & Custom Queries, rather than being covered briefly and incompletely on this one.