skip to content

How do you build a Message with custom headers, and why can't you just mutate headers on an existing message?

level: middleimportance: must knowfreq 65%

answer

  1. withPayload().setHeader().build()
  2. MessageHeaders immutable → UnsupportedOperationException
  3. fromMessage copies then rebuild
  4. setHeader(name, null) removes
  5. build() = new id+timestamp

basics

~10 s

Use MessageBuilder: MessageBuilder.withPayload(body).setHeader("key", value).build(). You can't mutate an existing message's headers because MessageHeaders is immutable — instead you build a new message, optionally copying the old headers.

solid answer

~40 s

You create messages with the fluent org.springframework.messaging.support.MessageBuilder: MessageBuilder.withPayload(payload).setHeader("orderId", 42).setHeader("type", "NEW").build(). It returns a GenericMessage (or ErrorMessage). Headers are immutable by design: MessageHeaders extends an unmodifiable map, so calling put/remove throws UnsupportedOperationException. This immutability makes messages safe to share across threads and channels without defensive copying, and guarantees that once id and timestamp are assigned they never change. When you need to 'change' a message you start from MessageBuilder.fromMessage(existing), which copies the payload and existing headers, then override or add headers and build a new instance — the original is untouched. build() generates a fresh id and timestamp for the new message unless you disable that behavior.

code

java · 16 lines
java
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

Message<String> original = MessageBuilder
    .withPayload("order-123")
    .setHeader("type", "NEW")
    .setHeaderIfAbsent("source", "api")
    .build();

// Derive a changed copy — original stays untouched
Message<String> updated = MessageBuilder
    .fromMessage(original)
    .setHeader("type", "UPDATED")
    .build();

// original.getHeaders().put("x", 1); // throws UnsupportedOperationException

go deeper

for a junior

Know the withPayload().setHeader().build() fluent chain.

for a middle

Explain immutability, fromMessage copy semantics, and the null-removes gotcha.

for a senior

Discuss why immutability enables safe cross-thread/channel sharing and stable identity.

for a principal

Reason about id/timestamp regeneration on copy and when to preserve identity via header accessor.

## Building messages While you *can* call `new GenericMessage<>(payload, headersMap)`, the idiomatic way is the fluent **`MessageBuilder`** (`org.springframework.messaging.support.MessageBuilder`): ```java Message<String> msg = MessageBuilder .withPayload("order-123") .setHeader("orderId", 42L) .setHeader("type", "NEW") .build(); ``` - `withPayload(T)` seeds the builder with the body. - `setHeader(name, value)` adds/overrides a single header. `setHeaderIfAbsent(...)` only sets when missing. `copyHeaders(map)` / `copyHeadersIfAbsent(map)` bulk-add. `removeHeader(name)` drops one. - `build()` produces an immutable `GenericMessage` (or `ErrorMessage` when the payload is a `Throwable`). ## Why headers are immutable `MessageHeaders` is a `Map<String, Object>` but **read-only**: it internally holds the entries and overrides mutating operations to throw `UnsupportedOperationException`. Reasons: 1. **Thread-safety / sharing** — the same `Message` may traverse multiple channels, endpoints, and threads. Immutability removes the need for defensive copies and prevents one consumer from corrupting metadata another relies on. 2. **Integrity of identity** — the auto-assigned `id` (UUID) and `timestamp` must be stable for correlation, deduplication, and logging. So `message.getHeaders().put("x", 1)` throws at runtime. ## Deriving a new message To 'modify', copy then rebuild: ```java Message<String> updated = MessageBuilder .fromMessage(original) // copies payload + all headers .setHeader("type", "UPDATED") .removeHeader("internalOnly") .build(); ``` The **original is unchanged**; `updated` is a new instance. Note `build()` normally assigns a **new `id` and `timestamp`**. If you need to preserve identity across the copy you can construct via a `MessageHeaderAccessor` and disable ID/timestamp generation (see the accessor question). ## Gotchas - Passing a raw map to `new GenericMessage<>(payload, map)` **also** wraps it immutably — but does *not* auto-generate id/timestamp if you supply your own map that already contains those keys; the constructor path is lower-level, which is why `MessageBuilder` is preferred. - `setHeader(name, null)` **removes** the header rather than storing a null value. - Header keys are plain strings; collisions silently overwrite. Prefer the constants in `MessageHeaders` for well-known keys.

  • What does setHeader(name, null) do?
    It removes the header entirely rather than storing a null value — a common surprise.
  • Does build() reuse the original id when using fromMessage?
    No — by default it assigns a fresh id and timestamp. Preserving identity requires a MessageHeaderAccessor with ID/timestamp generation disabled.

saying these in an interview costs you the question

  • Claiming you can call getHeaders().put(...) to add a header
  • Thinking fromMessage mutates the original
  • Believing setHeader(name, null) stores null

context