skip to content

How do user destinations work in Spring STOMP messaging (@SendToUser, /user prefix, convertAndSendToUser)?

level: middleimportance: must knowfreq 65%

answer

  1. /user prefix = private to me
  2. UserDestinationMessageHandler rewrites → /queue/x-user<session>
  3. convertAndSendToUser(user, dest-without-/user, payload)
  4. @SendToUser on @MessageMapping return; broadcast=false = one session
  5. needs Principal; cluster needs registry+destination broadcast

basics

~20 s

User destinations let you send to one specific user, not everyone. The client subscribes to /user/queue/... and the server sends via convertAndSendToUser(user, "/queue/...", payload) or returns from a @MessageMapping method annotated @SendToUser. Spring maps it to a session-unique queue.

solid answer

~40 s

A normal /topic broadcast reaches every subscriber; user destinations target one authenticated user. The client subscribes to a destination prefixed with /user, e.g. /user/queue/notifications. Spring's UserDestinationMessageHandler rewrites that per-session into a unique name like /queue/notifications-user<sessionId>, using the Principal on the session, so each of that user's sessions gets its own private queue. From code you push with SimpMessagingTemplate.convertAndSendToUser(username, "/queue/notifications", payload) — you pass the plain destination (no /user prefix, no session suffix) and the username; Spring resolves all active sessions for that user. From a @MessageMapping handler you can annotate the return with @SendToUser("/queue/reply") and it goes back to the calling user. It requires a Principal to identify 'who', and the destination prefix defaults to /user (configurable via setUserDestinationPrefix).

code

java · 24 lines
java
@Controller
public class NotificationController {

    private final SimpMessagingTemplate template;
    NotificationController(SimpMessagingTemplate template) { this.template = template; }

    // Declarative: reply privately to the calling user
    @MessageMapping("/greet")
    @SendToUser("/queue/greetings")            // -> /user/queue/greetings on that user's client
    public String greet(String name, Principal principal) {
        return "Hello " + principal.getName();
    }

    // Errors only to the originating session
    @MessageExceptionHandler
    @SendToUser(destinations = "/queue/errors", broadcast = false)
    public String handle(Exception ex) { return ex.getMessage(); }

    // Imperative: push to a user from anywhere (pass username + base destination)
    public void notify(String username, Notification n) {
        template.convertAndSendToUser(username, "/queue/notifications", n);
    }
}
// Client subscribes to:  /user/queue/greetings , /user/queue/notifications

go deeper

for a junior

Know that /user destinations target one user and you subscribe to /user/queue/... .

for a middle

Explain the rewrite by UserDestinationMessageHandler, both send styles, and the Principal requirement.

for a senior

Discuss broadcast=false semantics, @SendToUser vs @SendTo, and error-handler patterns.

for a principal

Address multi-instance delivery via user registry/destination broadcast and the simple-broker limitation.

**The problem user destinations solve:** With a plain broker destination like `/topic/prices`, *every* subscriber receives every message. Sometimes you need to send to **one specific user** — a private notification, a reply to their own request, an error meant only for them. User destinations are Spring's built-in mechanism for that, without you manually tracking session IDs. **The three moving parts:** 1. **Client subscribes under `/user`** — e.g. `stompClient.subscribe('/user/queue/notifications', cb)`. The `/user` prefix is special (default; configurable via `MessageBrokerRegistry.setUserDestinationPrefix`). It means "a destination private to *me*." 2. **`UserDestinationMessageHandler` translation** — Spring intercepts destinations starting with `/user`. For a SUBSCRIBE, it rewrites `/user/queue/notifications` into a **session-unique** actual destination, roughly `/queue/notifications-user<sessionId>`. Each WebSocket session of that user gets its own unique suffix, so two browser tabs of the same person each have a distinct real queue but both subscribed via the same `/user/...` name. 3. **Server sends** two ways: - **Imperatively:** `SimpMessagingTemplate.convertAndSendToUser(user, destination, payload)`. You pass the **username** and the **base destination without the /user prefix and without any session suffix**, e.g. `convertAndSendToUser("alice", "/queue/notifications", dto)`. Spring looks up *all* active sessions for user "alice" in the `SimpUserRegistry` and delivers a copy to each session's unique queue. - **Declaratively:** annotate a `@MessageMapping` handler method's return value with `@SendToUser`. `@SendToUser("/queue/reply")` routes the returned object back to the **same user that sent the inbound message**. `@SendToUser(destinations="/queue/errors", broadcast=false)` (broadcast=false) sends only to the originating **session** rather than all of that user's sessions — useful for request-scoped replies and exceptions. **How 'who is the user' is determined:** every message carries a `Principal` (the authenticated user) resolved from the WebSocket session — established at HTTP handshake time or set on the STOMP CONNECT frame by a `ChannelInterceptor`. `convertAndSendToUser`'s `user` argument is matched against `Principal.getName()`. No principal → no user destination. **@SendToUser vs @SendTo:** `@SendTo("/topic/x")` broadcasts the return value to a shared destination (everyone). `@SendToUser("/queue/x")` sends it privately to the current user. You can also combine `@MessageMapping` with these; without either annotation, the return value goes by default to `/topic/<inbound-destination>` (broker-prefixed) — a common surprise. **Error handling pattern:** `@MessageExceptionHandler` + `@SendToUser("/queue/errors")` sends exceptions from message handling back to just the offending user's session. **Cluster gotcha:** `convertAndSendToUser` resolves sessions from the **local** `SimpUserRegistry` only. If user "alice" is connected to server B but your send happens on server A, she won't get it — unless you enable **user registry + user destination broadcast** across the cluster (see the STOMP broker relay with `setUserRegistryBroadcast`/`setUserDestinationBroadcast`). With the simple in-memory broker there is no cross-server support. **Destination convention:** by convention user-specific destinations use `/queue/...` (point-to-point semantics) rather than `/topic/...`, and you must have declared `/queue` (or your chosen prefix) in `enableSimpleBroker("/topic", "/queue")` or via the relay. The `/user` prefix itself is *not* a broker prefix — it's handled by `UserDestinationMessageHandler` before the broker sees the rewritten destination.

  • What does broadcast=false on @SendToUser change?
    By default @SendToUser (and convertAndSendToUser) delivers to ALL of the user's active sessions. broadcast=false restricts delivery to only the session that sent the current message — ideal for request-specific replies and exception messages so other tabs/devices don't receive them.
  • When you call convertAndSendToUser, do you include the /user prefix or the session-id suffix in the destination?
    Neither. You pass the plain base destination like "/queue/notifications" plus the username. Spring's UserDestinationMessageHandler adds the /user resolution and the per-session suffix internally for every active session of that user.
  • Why might convertAndSendToUser silently deliver nothing in a multi-instance deployment?
    convertAndSendToUser resolves sessions from the local SimpUserRegistry. If the user is connected to a different instance, you need cross-server user registry broadcast and user destination broadcast (via the STOMP broker relay); the in-memory simple broker doesn't support it.

saying these in an interview costs you the question

  • Passing /user/queue/... or a session suffix to convertAndSendToUser (you pass the base destination + username)
  • Thinking @SendTo and @SendToUser are interchangeable (one broadcasts, one is private)
  • Assuming convertAndSendToUser works across a cluster with the simple in-memory broker
  • Believing user destinations work without a Principal on the session

context