skip to content

What is the difference between a SubscribableChannel and a PollableChannel (e.g. DirectChannel vs QueueChannel)?

level: middleimportance: must knowfreq 45%

answer

  1. Subscribable = push + subscribe()
  2. Pollable = pull + receive()
  3. DirectChannel = sender's thread, sync, point-to-point
  4. QueueChannel = BlockingQueue, needs a poller
  5. no subscriber → send fails; empty queue → receive returns null

basics

~10 s

A SubscribableChannel pushes each message to handlers that subscribed to it (DirectChannel). A PollableChannel buffers messages in a queue and a consumer must pull them with receive() (QueueChannel).

solid answer

~40 s

Both extend MessageChannel but differ in delivery model. A SubscribableChannel adds subscribe(MessageHandler)/unsubscribe — it actively pushes each incoming message to its subscribed handlers, so a handler must be registered before send. DirectChannel is the canonical one: it dispatches in the sender's own thread, synchronously, point-to-point. PublishSubscribeChannel broadcasts to all subscribers. A PollableChannel instead adds receive()/receive(timeout) — it buffers messages internally and does nothing until a consumer pulls them. QueueChannel is the canonical one, backed by a BlockingQueue; you attach a poller (in Spring Integration) or call receive() yourself, which decouples sender and receiver in time and thread. Rule of thumb: SubscribableChannel = push, synchronous by default, no buffering; PollableChannel = pull, buffered, needs a poller/receiver to drain it.

code

java · 13 lines
java
import org.springframework.messaging.*;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.integration.channel.*;

// SubscribableChannel: push, handler must be subscribed first
SubscribableChannel direct = new DirectChannel();
direct.subscribe(m -> System.out.println("pushed: " + m.getPayload()));
direct.send(new GenericMessage<>("A")); // handler runs now, same thread

// PollableChannel: buffer, must pull
PollableChannel queue = new QueueChannel(10); // bounded, capacity 10
queue.send(new GenericMessage<>("B"));       // just enqueued
Message<?> pulled = queue.receive(1000);      // returns "B" or null after 1s

go deeper

for a junior

Push vs pull: DirectChannel pushes, QueueChannel buffers and you pull with receive().

for a middle

Map each sub-interface to its methods and canonical implementation, plus the no-subscriber vs empty-queue behaviors.

for a senior

Discuss threading, transaction propagation, back-pressure, and where failures surface for each.

for a principal

Reason about topology trade-offs (latency vs decoupling vs error handling) when picking channel types across a flow.

## Two delivery contracts `MessageChannel` has exactly two sub-interfaces, and they encode *who initiates delivery*. ### SubscribableChannel — push `org.springframework.messaging.SubscribableChannel` adds: - `boolean subscribe(MessageHandler handler)` - `boolean unsubscribe(MessageHandler handler)` Handlers register up front. When a producer calls `send()`, the channel immediately *pushes* the message to its subscriber(s). No internal buffer. If **no handler is subscribed**, sending typically fails (a `DirectChannel` throws `MessageDeliveryException: Dispatcher has no subscribers`). Implementations: - **`DirectChannel`** — the default channel type. Point-to-point: exactly one handler receives each message, using a `UnicastingDispatcher` with round-robin load-balancing across multiple subscribers. Crucially it dispatches **in the caller's thread**, synchronously — so the sender's thread, transaction, and security context carry into the handler, and handler exceptions propagate back to `send()`. - **`PublishSubscribeChannel`** — broadcasts each message to *all* subscribers (fan-out). - **`ExecutorChannel`** — like DirectChannel but hands each dispatch to a `TaskExecutor`, so delivery is asynchronous on a pool thread (breaking the single-thread/transaction guarantee). ### PollableChannel — pull `org.springframework.messaging.PollableChannel` adds: - `Message<?> receive()` - `Message<?> receive(long timeout)` The channel **buffers** messages. Sending just enqueues; nothing is processed until something *pulls*. In Spring Integration a **poller** (`@Poller` / `PollerMetadata`) periodically calls `receive()` on a scheduler thread and feeds the message to the downstream handler. You can also call `receive()` directly. Implementation: - **`QueueChannel`** — backed by a `BlockingQueue`, optionally **bounded** (`new QueueChannel(capacity)`). When full, `send()` blocks up to the timeout and returns `false` if it can't enqueue. This gives you buffering, back-pressure, and thread/temporal decoupling. Related: `PriorityChannel`, `RendezvousChannel` (zero-capacity handoff). ## Consequences that matter in interviews - **Threading & transactions:** DirectChannel keeps everything on one thread — a `@Transactional` boundary spanning the whole flow works. QueueChannel (and ExecutorChannel) crosses thread boundaries, so the transaction/security context does **not** propagate; each poll runs in its own thread/transaction. - **Ordering & throughput:** DirectChannel is simple and preserves caller ordering but offers no buffering or back-pressure. QueueChannel absorbs bursts but adds latency (poll interval) and needs a poller configured or messages sit forever. - **Failure surfacing:** With DirectChannel a downstream failure is thrown to the producer synchronously. With QueueChannel the producer is long gone; errors surface on the poller thread and go to an error channel, not the sender. - **No subscribers vs empty queue:** SubscribableChannel with no subscriber → send fails immediately. PollableChannel with nothing to receive → `receive(timeout)` returns `null` after the timeout. ## When to use which - Use **DirectChannel** (default) for straight-through, synchronous, transactional flows where you want simplicity and error propagation. - Use **QueueChannel** when you need buffering, to decouple producer speed from consumer speed, or to move work onto another thread — accepting the loss of a shared transaction and the need for a poller. - Use **PublishSubscribeChannel** for fan-out to multiple independent consumers.

  • You put a QueueChannel in a flow but no messages ever get processed. Why?
    A QueueChannel only buffers — something must drain it. In Spring Integration you need a poller on the consuming endpoint (e.g. @Poller / PollerMetadata); without a receiver calling receive(), messages just accumulate.
  • Which channel type lets a @Transactional boundary span the whole send→handle flow, and why?
    DirectChannel, because it dispatches synchronously in the sender's thread — the same transaction and thread-bound context are active in the handler. QueueChannel/ExecutorChannel cross threads, so the transaction doesn't carry over.

saying these in an interview costs you the question

  • Saying QueueChannel pushes to handlers (it buffers; consumers pull)
  • Thinking DirectChannel uses a separate thread pool by default
  • Believing a QueueChannel processes messages without a poller/receiver
  • Assuming transactions propagate across a QueueChannel

context