Recreating WeChat‑style Message Recall in Spring Boot: Why Deleting Isn’t Enough
The article walks through a complete Spring Boot implementation of WeChat‑like message recall, explaining why a simple DELETE is insufficient and detailing how to use a status flag, conditional UPDATE, WebSocket, Redis/MQ, and multi‑device session management to synchronize recall across all clients, even under concurrency and offline scenarios.
1. The initial misconception: DELETE is not enough
When a user long‑presses a message, the naive backend action is DELETE FROM chat_message WHERE id = ?;. Deleting the row only removes it from the database; the message still exists on the sender’s phone, the receiver’s phone, web clients, Redis caches, WebSocket connections, and offline message lists. Real‑time chat systems need to keep a record of the recall event and display a placeholder like "You recalled a message".
2. Redesigning the schema
Instead of deleting, add a status column (VARCHAR(20) default 'NORMAL') and a recalled_at timestamp to chat_message. Define an enum:
public enum MessageStatus {
NORMAL,
RECALLED
}When a message is recalled, set status = RECALLED and optionally mask the original content.
3. Sending a message
The send endpoint stores the message in MySQL, sets status = NORMAL, and pushes it via Spring’s native WebSocket support to all online devices.
@PostMapping("/messages")
public SendMessageResult send(@RequestBody SendMessageRequest request, Authentication authentication) {
Long senderId = currentUserId(authentication);
return messageService.send(senderId, request);
}4. Recall API constraints
Recall is allowed only if:
the requester is the sender
the message is still NORMAL the recall occurs within a defined time window (e.g., 2 minutes)
These rules are enforced in a single conditional UPDATE:
UPDATE chat_message
SET status = 'RECALLED', recalled_at = NOW()
WHERE id = :messageId
AND sender_id = :senderId
AND status = 'NORMAL'
AND created_at >= :deadline;The Java service checks the affected row count; if it is not 1, a MessageRecallException is thrown.
5. Publishing the recall event
After a successful UPDATE, a MessageRecalledEvent is created and published:
eventPublisher.publish(new MessageRecalledEvent(
message.getId(),
message.getConversationId(),
message.getSenderId(),
message.getReceiverId()
));The WebSocket layer receives the event, looks up all sessions belonging to the sender and receiver, and pushes a notification that replaces the original content with a placeholder such as "You recalled a message" or "The other party recalled a message".
6. Handling multiple devices per user
Each user may have several active sessions (e.g., iPhone, Mac, Chrome). Instead of a single WebSocket connection per user, store a collection of device sessions so that a recall event can be broadcast to every online endpoint of both participants.
7. Scaling to multiple Spring Boot instances
When the service runs on several nodes behind a gateway, a recall event generated on one node must be visible to others. The solution is to publish the event to a shared channel (Redis Pub/Sub, Kafka, RocketMQ, etc.). All nodes subscribe, locate the relevant WebSocket sessions locally, and push the recall notification.
8. Offline recipients
If the receiver is offline when the recall occurs, the database already reflects the RECALLED status. When the user reconnects and queries the chat history, the placeholder is shown, ensuring eventual consistency without relying on the missed real‑time WebSocket message.
9. Full recall workflow
User sends message → save to MySQL → WebSocket push to all devices
User clicks recall → Java validates sender & time window → conditional UPDATE (NORMAL → RECALLED)
Generate MESSAGE_RECALLED event → publish via MQ/Redis → each node pushes to all sender/receiver sessions
Offline devices later query DB and see RECALLED status → display placeholderThe implementation demonstrates key backend concepts: state machines, conditional updates, idempotency, concurrency control, WebSocket integration, multi‑device session management, cross‑instance event propagation, offline synchronization, and eventual consistency.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
