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.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
Recreating WeChat‑style Message Recall in Spring Boot: Why Deleting Isn’t Enough

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 placeholder

The 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyredisspring-bootmysqlwebsocketmqmessage-recall
Java Tech Enthusiast
Written by

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!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.