Recreating WeChat‑Style Message Recall with Spring Boot: Why Deleting Isn’t the Hard Part
The article walks through building a true WeChat‑like message recall feature in Spring Boot, explaining why simply deleting a row is insufficient and detailing the required status change, concurrency‑safe conditional update, multi‑device session handling, and cross‑instance event propagation.
When implementing instant‑messaging, the author initially thought that recalling a message was as simple as executing DELETE FROM chat_message WHERE id = ?, but quickly discovered that a message may already reside on the sender’s phone, the receiver’s phone, web clients, Redis, WebSocket connections, and offline queues, so deleting the row does not remove it from users’ screens.
The real recall operation is to change a message’s state from NORMAL to RECALLED and synchronize this state to every connected device.
1. Redesign the schema
The original chat_message table only stored the message content. To support recall, two new columns are added:
ALTER TABLE chat_message ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'NORMAL';
ALTER TABLE chat_message ADD COLUMN recalled_at DATETIME NULL;An enum defines the possible states: public enum MessageStatus { NORMAL, RECALLED } When a message is recalled, its content may be kept or masked, status is set to RECALLED, and recalled_at records the current time.
2. Sending a message
A simple REST endpoint stores the message in MySQL and then pushes it to online users via Spring’s native WebSocket support:
@PostMapping("/messages")
public SendMessageResult send(@RequestBody SendMessageRequest request, Authentication authentication) {
Long senderId = currentUserId(authentication);
return messageService.send(senderId, request);
}
@Transactional
public SendMessageResult send(Long senderId, SendMessageRequest request) {
ChatMessage message = new ChatMessage();
message.setId(idGenerator.nextId());
message.setConversationId(request.conversationId());
message.setSenderId(senderId);
message.setReceiverId(request.receiverId());
message.setContent(request.content());
message.setStatus(MessageStatus.NORMAL);
message.setCreatedAt(LocalDateTime.now());
messageRepository.save(message);
return SendMessageResult.from(message);
}After persisting, a MessageEvent is generated and sent through the WebSocket channel so that every online client displays the new message.
3. Recall API constraints
The system enforces business rules: only the sender may recall, the recall must occur within two minutes of sending, and a message can be recalled only once.
@PostMapping("/messages/{messageId}/recall")
public void recall(@PathVariable Long messageId, Authentication authentication) {
messageService.recall(messageId, currentUserId(authentication));
}4. Concurrency‑safe conditional UPDATE
Rather than a SELECT followed by an UPDATE, a single SQL statement checks all conditions atomically:
UPDATE chat_message
SET status = 'RECALLED',
recalled_at = NOW()
WHERE id = :messageId
AND sender_id = :senderId
AND status = 'NORMAL'
AND created_at >= :deadline;In Java the affected row count is examined:
int affected = messageRepository.recall(messageId, senderId, LocalDateTime.now().minusMinutes(2));
if (affected != 1) {
throw new MessageRecallException("Message not found, already recalled, or past recall window");
}If two devices attempt to recall simultaneously, only one will succeed (affectedRows = 1); the other receives 0 rows and fails gracefully, eliminating the need for explicit locks.
5. Publishing the recall event
After the database update, a MessageRecalledEvent is published:
eventPublisher.publish(new MessageRecalledEvent(
message.getId(),
message.getConversationId(),
message.getSenderId(),
message.getReceiverId()
));In a single‑instance deployment the event stays in JVM memory; in a multi‑instance deployment it is forwarded via Redis Pub/Sub, Kafka, RocketMQ, or Redis Streams so that every node can deliver the event to its own WebSocket sessions.
6. WebSocket handling
The WebSocket layer receives the MESSAGE_RECALLED event, looks up all sessions belonging to the sender and receiver (including multiple devices), and pushes a payload such as: {"event":"MESSAGE_RECALLED","messageId":10001} Clients replace the original content with a placeholder like “You recalled a message” or “The other party recalled a message”.
7. Multi‑device session management
Because a user may be logged in on several devices, the server stores a collection of sessions per userId instead of a single WebSocket connection:
10086 →
├─ iphone‑session
├─ mac‑session
└─ chrome‑sessionWhen a recall occurs, the event is sent to all of the user’s active sessions as well as the counterpart’s sessions.
8. Scaling to many instances
With a gateway routing to multiple Spring Boot instances, a recall request may hit instance A while the receiver’s WebSocket connection lives on instance B. Therefore the recall event must be persisted (e.g., in Redis or a message queue) and broadcast to all nodes, which then forward it to the appropriate WebSocket sessions.
9. Offline recipients
If the receiver is offline, the database already reflects status = RECALLED. When the client reconnects and queries the conversation history, it receives the updated status and displays “The other party recalled a message”, ensuring eventual consistency even without a real‑time WebSocket push.
10. Full recall workflow
User sends message → save to MySQL → WebSocket pushes to all devices
User clicks recall → Java validates sender & time → conditional UPDATE (NORMAL→RECALLED)
→ generate MESSAGE_RECALLED event → MQ/Redis propagates across nodes
→ each node pushes to all sender/receiver sessions → clients replace content with placeholder
→ offline devices sync from DB on next queryThe implementation showcases several backend concepts: state machines, conditional updates, idempotency, concurrency control, WebSocket integration, multi‑device session tracking, cross‑instance message 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.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
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.
