skip to content

Compare DefaultMessageListenerContainer and SimpleMessageListenerContainer. When would you choose each, and how does concurrency differ?

level: middleimportance: must knowfreq 50%

answer

  1. DMLC = polling loop -> transactions + recovery + scaling
  2. SMLC = provider push, fixed consumers
  3. external/JTA TM = DMLC only
  4. concurrency range ignored by SMLC
  5. Boot/default factory build DMLC

basics

~20 s

DefaultMessageListenerContainer (DMLC) is the flexible default: it supports transactions, error recovery, and dynamic scaling of consumer threads (concurrency "lower-upper"). SimpleMessageListenerContainer (SMLC) is lightweight with a fixed number of consumers and no external transaction support. Use DMLC almost always.

solid answer

~40 s

Both run @JmsListener endpoints but differ fundamentally. DMLC uses a polling loop (receive() with a timeout) on each consumer thread; this lets it support local JMS transactions AND an external JtaTransactionManager, recover automatically after broker/connection failures, and dynamically scale consumers between a lower and upper bound (setConcurrency("3-10")). SMLC registers a plain JMS MessageListener and relies on the provider's async delivery; it has a fixed consumer count, no dynamic scaling, no external transaction-manager support (only local session transactions), and weaker recovery. SMLC has slightly lower overhead and no polling gap, but you lose transactions/scaling/recovery. In practice DMLC is the near-universal choice and is what DefaultJmsListenerContainerFactory (and Spring Boot) creates. Pick SMLC only for simple, non-transactional, fixed-load cases where you want minimal overhead and provider-driven push.

code

java · 20 lines
java
// DMLC via factory: dynamic scaling + external JTA transaction manager
@Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(
        ConnectionFactory cf, JtaTransactionManager tm) {
    var factory = new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(cf);
    factory.setConcurrency("3-10");        // grows 3 -> 10 under load (DMLC only)
    factory.setTransactionManager(tm);     // XA across JMS + DB (DMLC only)
    return factory;
}

// SMLC via factory: fixed consumers, no external TM support
@Bean
public SimpleJmsListenerContainerFactory simpleFactory(ConnectionFactory cf) {
    var factory = new SimpleJmsListenerContainerFactory();
    factory.setConnectionFactory(cf);
    factory.setConcurrency("5");           // fixed 5; a range would NOT scale
    factory.setSessionTransacted(true);    // only local transactions available
    return factory;
}

go deeper

for a junior

Know DMLC is the default and more capable; SMLC is the lightweight one.

for a middle

Contrast polling-loop vs provider-push and the resulting transaction/recovery/scaling differences.

for a senior

Reason about receiveTimeout, recoveryInterval, idle consumer limits, and when SMLC's lower overhead actually matters.

for a principal

Weigh container choice against delivery guarantees, resource holding, and operational recovery for a given SLA; justify defaulting to DMLC org-wide.

## The two containers A **message listener container** is the object that opens JMS sessions/consumers, receives messages, and dispatches them to your `@JmsListener` method. Spring ships two concrete implementations. ### DefaultMessageListenerContainer (DMLC) Extends `AbstractPollingMessageListenerContainer`. Each consumer thread runs a **loop**: `consumer.receive(timeout)` → if a message arrives, dispatch to the listener; otherwise loop again after the `receiveTimeout` (default 1s). This polling model is what unlocks its capabilities: - **Transactions:** because each receive+process cycle is under the container's control, DMLC can wrap it in a **local JMS transaction** (`sessionTransacted=true`) or delegate to an **external `PlatformTransactionManager`** — including `JtaTransactionManager` for XA transactions spanning JMS + a database. This is the big one: **only DMLC supports an external transaction manager.** - **Recovery:** if the broker or connection dies, DMLC catches the exception, backs off, and re-establishes the session/consumer (`recoveryInterval`, default 5s). It self-heals. - **Dynamic scaling:** `setConcurrency("3-10")` (or `setConcurrentConsumers(3)` + `setMaxConcurrentConsumers(10)`) starts 3 consumers and can grow to 10 under load, then shrink. Governed by `idleConsumerLimit`, `maxMessagesPerTask`, `idleTaskExecutionLimit`. - **Cost:** the polling loop means a thread may briefly block on `receive` and there is a small latency floor equal to loop scheduling; also each consumer holds a session. ### SimpleMessageListenerContainer (SMLC) Extends `AbstractMessageListenerContainer`. It creates a fixed set of `MessageConsumer`s and registers a JMS `MessageListener` on each — delivery is **pushed** by the JMS provider's own threads/async dispatch. Consequences: - **Fixed concurrency:** `setConcurrentConsumers(n)` fixes the count. A range like `"3-10"` is **not** honored as dynamic scaling — SMLC only reads the lower value; there is no `maxConcurrentConsumers`. - **No external transaction manager:** it supports only **local session transactions** (`sessionTransacted=true`). You **cannot** plug in `JtaTransactionManager` / XA. - **Weaker recovery:** it does not have DMLC's re-establishment loop; connection recovery depends more on the provider/`ConnectionFactory`. - **Lower overhead / no poll gap:** no receive-timeout loop, so slightly lower latency and CPU for steady simple workloads. ## Choosing - **Default to DMLC.** You want transactions, redelivery-on-exception, broker-failure recovery, and elastic consumers — all standard production needs. `DefaultJmsListenerContainerFactory` and Spring Boot produce DMLC. - **SMLC** only when: no transactions needed, fixed load, minimal overhead desired, and you accept weaker recovery. Rare in practice. ## Configuration entry points You usually don't `new` these; you configure a `JmsListenerContainerFactory`: - `DefaultJmsListenerContainerFactory` → DMLC (supports `setTransactionManager`, `setConcurrency("l-u")`, `setSessionTransacted`). - `SimpleJmsListenerContainerFactory` → SMLC. ## Gotchas - Setting `concurrency="3-10"` on an SMLC-based factory silently gives you 3 fixed consumers — no scaling. - Expecting XA/JTA to work with SMLC → it won't; you must use DMLC. - DMLC's `receiveTimeout` interacts with transaction timeouts; a very long `receiveTimeout` under a transactional consumer can hold resources.

  • You set concurrency to "3-10" but consumers never scale beyond 3. What's a likely cause?
    The container is a SimpleMessageListenerContainer, which ignores the upper bound and runs a fixed count. Dynamic scaling requires a DefaultMessageListenerContainer.
  • Why can DMLC support an external transaction manager while SMLC cannot?
    DMLC drives message reception itself in a receive()-then-process loop, so it can start/commit a transaction around each cycle. SMLC hands control to the provider's async push, so there's no place to bracket an external transaction.

saying these in an interview costs you the question

  • Claiming SMLC supports XA/JtaTransactionManager.
  • Thinking both containers scale dynamically with a concurrency range.
  • Assuming SMLC recovers from broker failures like DMLC does.

context