How do user destinations work in Spring STOMP messaging (@SendToUser, /user prefix, convertAndSendToUser)?
answer
- /user prefix = private to me
- UserDestinationMessageHandler rewrites → /queue/x-user<session>
- convertAndSendToUser(user, dest-without-/user, payload)
- @SendToUser on @MessageMapping return; broadcast=false = one session
- needs Principal; cluster needs registry+destination broadcast
basics
~20 sUser 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 sA 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@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/notificationsgo deeper
Know that /user destinations target one user and you subscribe to /user/queue/... .
Explain the rewrite by UserDestinationMessageHandler, both send styles, and the Principal requirement.
Discuss broadcast=false semantics, @SendToUser vs @SendTo, and error-handler patterns.
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