Designing Message Edit & Delete Consistency for Offline Recipients
How messaging platforms guarantee that every recipient — online right now or reconnecting three days from now — converges on the exact same final view of a conversation, even after edits and deletions happen mid-flight.
Introduction & History
When messaging apps first appeared, a sent message was permanent. SMS had no concept of editing or unsending — once a text left your phone, it existed forever on every recipient’s device, typos and all. Early internet chat (IRC, early instant messengers) inherited the same assumption: messages were an append-only stream, and the only way to “fix” a mistake was to send a follow-up message. This mental model — messages as immutable, append-only events — is actually foundational to how modern systems still work under the hood, even though the user experience has evolved dramatically.
1.1 A Brief History of Editable Messaging
SMS goes mainstream
Text messages are strictly append-only: once sent, they exist forever on every recipient’s device with no protocol-level notion of edit or unsend. The mental model that would shape early messaging is set in stone.
IRC and early IM
Internet chat inherits the append-only assumption. The only fix for a mistake is to send a correcting follow-up message.
WhatsApp “Delete for Everyone”
Users get a short window to retract a message from every recipient’s device, not just their own. The distributed-systems problem of “undoing” a delivered message enters the mainstream.
Telegram: unlimited edit & delete
Edits and deletions with no time limit. Messages become fully mutable indefinitely, placing correspondingly higher demands on long-term event history and sync correctness for very old messages.
iMessage adds edit within 15 minutes
Recipients see both the edit and a visual history of prior versions. A deliberate product choice that time-boxes how long the convergence problem stays open per message.
Slack & Discord: indefinite editing
Built for longer-lived, searchable conversations where messages often function as a persistent knowledge record. Editing is expected to work forever.
The shift began as messaging platforms started treating conversations less like a transcript and more like a shared, mutable document that multiple parties observe simultaneously. WhatsApp introduced “Delete for Everyone” in 2017, giving users a short window to retract a message from every recipient’s device, not just their own. Telegram went further, allowing edits and deletions with no time limit at all, and iMessage later added the ability to edit a sent message within a 15-minute window, with the recipient seeing both the edit and a visual history of prior versions. Slack and Discord, built for longer-lived, searchable conversations rather than ephemeral chat, allow indefinite editing since messages there often function as a persistent knowledge record.
What all of these products have in common, underneath very different UX decisions, is a hard distributed systems problem: a message, once sent, may already be in flight to — or already displayed on — devices you have no live connection to. An edit or delete is not just a database update; it is a correction that has to be redelivered, tracked, and eventually applied everywhere that message ever existed, including devices that are currently powered off, in airplane mode, or simply have not opened the app in a week. This is the central engineering problem this document works through end to end.
1.2 Why Event Sourcing Is the Right Foundation
There is a deeper reason event sourcing is the right foundation here, beyond convenience: it converts a distributed-consistency problem into a much more tractable replay problem. In a naive in-place-mutation design, “did device A see the same thing as device B” depends on a tangled combination of network timing, which server handled which request, and whatever partial state happened to be visible at each read. In an event-sourced design, that question reduces to “have device A and device B applied the same prefix of the same ordered event stream” — a question you can answer precisely, test deterministically, and reason about with the same tools used to verify replication correctness in databases generally. This reframing is why nearly every serious messaging platform, regardless of their specific product decisions about edit windows or delete semantics, converges on some variant of this architecture under the hood.
- Why could not SMS support message editing, architecturally?
- What is the core distributed-systems challenge that message editing introduces beyond a simple database UPDATE?
- Why might Slack allow indefinite editing while WhatsApp historically limited “delete for everyone” to a time window?
- Why does event sourcing turn a hard consistency problem into an easier replay problem?
1.3 What Makes This Problem Hard
Recipients may already be offline
A mutation may need to reach a device that is powered off, in airplane mode, or unopened for weeks. The system cannot rely on a live connection at the moment of edit or delete.
Devices arrive in any order
Network delivery is not guaranteed to preserve order — a delete can plausibly arrive at a device before the original send does. The ordering scheme has to handle every interleaving.
Multiple devices per user
Phone, tablet, laptop, web client. All must converge on identical state, and none can be assumed to be “the authoritative one.”
Convergence must be eventual, not optional
Two recipients seeing permanently different content is a correctness bug, not a performance blip. The whole design has to make divergence impossible by construction.
Architecture & Core Components
Supporting edits and deletes reliably requires re-framing how you think about “sending a message” in the first place. A message is not just content — it is an entity with a lifecycle, a version history, and a delivery status that must be tracked per recipient, per device.
Ingestion Layer
API Gateway and the Message Service that accept new sends, edits, and delete requests, validate them, and assign them a durable, ordered identity.
Source of Truth Store
The Message Store (append-only event log plus a materialized current-state table) that holds every message’s full version history and current status.
Fan-Out & Delivery Layer
The Fan-Out Service, per-device Delivery Queues, and the Real-Time Gateway (WebSocket or long-lived connections) that push live updates to online devices.
Offline Reconciliation Layer
The Sync Service and per-device Cursor / Checkpoint Store that let a reconnecting device catch up on everything it missed — new messages, edits, and deletes — in the correct order.
Figure 1 — End-to-end architecture. Blue lines are control and metadata; red lines are real-time delivery of mutations; purple dashed lines are wake-up hints (never the delivery mechanism for content itself).
2.1 Component Responsibilities
| Component | Responsibility |
|---|---|
| Message Service | The single write path for send, edit, and delete operations. Assigns each mutation a monotonically increasing sequence number scoped to the conversation, so ordering is well-defined regardless of which server handled the request. |
| Append-Only Event Log | Records every mutation (send, edit, delete) as an immutable event — never overwritten, never lost — which is the ultimate source of truth for reconstructing any device’s view at any point in time. |
| Materialized Message State Store | A derived, queryable table holding each message’s current content and status (active, edited, deleted), rebuilt from the event log, optimized for fast reads when rendering a conversation. |
| Fan-Out Service | Takes a mutation event and determines every recipient device that needs to see it, then routes delivery through the Real-Time Gateway (if online) or Push Notification Service (if offline). |
| Real-Time Gateway | Maintains persistent WebSocket (or equivalent) connections to online devices and pushes mutation events the instant they occur. |
| Per-Device Cursor Store | Tracks, for every device, the last event sequence number it has successfully applied — the key to correct, gap-free catch-up on reconnect. |
| Sync Service | Answers “what changed since cursor X” for a reconnecting device, replaying the event log (sends, edits, deletes) in order until the device is fully caught up. |
2.2 Why a Separate Materialized State Store Instead of Reading the Log Directly
It is tempting to ask: if the event log has everything, why not just read directly from it every time a client needs to render a conversation? The answer is a classic read-versus-write optimization trade-off. The event log is optimized for durable, ordered appends and sequential replay — exactly what sync needs. But rendering “open this conversation right now” needs fast, random-access reads of current state, ideally without replaying potentially thousands of historical events per message just to determine what to show. The materialized state store exists purely to serve that read pattern cheaply, kept eventually consistent with the log through the same fan-out mechanism that delivers events to clients. This is the same read / write-path separation that CQRS (Command Query Responsibility Segregation) formalizes in general distributed-systems design, applied here to a very concrete, very high-traffic use case.
2.3 Why the Cursor Store Deserves Its Own Component
It would be simpler, on paper, to derive “what has this device seen” implicitly from delivery acknowledgments scattered across logs. In practice, that makes correctness nearly impossible to verify or reason about at scale — you would need to reconstruct a device’s exact sync position from scattered evidence every time it reconnects, which is slow and error-prone. Making the cursor an explicit, first-class, directly-queryable piece of state turns “what does this device need next” into a single, cheap lookup, and makes the entire sync protocol auditable: you can inspect any device’s cursor at any time and know exactly what it has and has not seen, which is invaluable both for debugging production issues and for building product features like accurate “delivered” and “seen” indicators.
- Why is an append-only event log preferred over simply UPDATE-ing a message row in place?
- What does the per-device cursor actually need to encode — is a timestamp sufficient, or do you need something stronger?
- Why maintain both an event log and a separate materialized state store instead of just one or the other?
- Why is it worth making the cursor an explicit, first-class piece of state rather than deriving it implicitly?
Internal Working — How Edits and Deletes Actually Propagate
This is the heart of the design. Let’s walk through exactly what happens, mechanically, when a user edits or deletes a message.
3.1 Messages as an Append-Only Event Stream
The critical design decision that makes this whole problem tractable is: never mutate a message in place at the storage layer. Instead, every action — original send, edit, delete — is recorded as a new, immutable event referencing the original message’s ID. A message’s “current state” is always a derived value, computed by replaying its event history: original content, followed by the latest edit (if any), followed by a delete marker (if any). This is the same event-sourcing principle used in financial ledgers, and for good reason: it gives you a complete, replayable audit trail, and — crucially for this problem — it gives every device, no matter how far behind, a well-defined way to catch up by simply replaying events it has not seen yet, in order.
Think of a shared Google Doc where instead of overwriting a paragraph, every keystroke, correction, and deletion is logged as an ordered operation. Anyone opening the doc — now or a year from now, online or offline until yesterday — can arrive at the identical current text simply by applying every operation, in order, from the beginning (or from the last snapshot they saw). Message edit and delete works the same way, except the “paragraphs” are individual messages, and the “doc” is a conversation.
3.2 Ordering Within a Conversation
Every mutation needs a well-defined position in a total order, scoped per conversation, so that a device applying events out of arrival order (which will happen — network delivery is not guaranteed to preserve order) can still reconstruct the correct final state. The standard approach is a monotonically increasing sequence number assigned by the Message Service at write time, backed by a single ordering authority per conversation shard (not a wall-clock timestamp, which is unreliable across distributed servers due to clock skew). An edit or delete event carries both its own new sequence number and a reference to the original message’s sequence number, so any device can determine “this edit applies to message #482” regardless of when the edit event physically arrives.
3.3 Applying an Edit
- Sender submits an edit request referencing the original message ID and new content.
- Message Service validates the request (message exists, sender owns it, still within any platform-defined edit window) and writes a new
MessageEditedevent to the log with a fresh sequence number. - The materialized state store is updated: the message’s current content pointer now points to the new version; the original content is retained in history, not discarded.
- Fan-Out Service reads the event and determines the recipient set (all active conversation members, all their registered devices).
- For each recipient device: if online, the Real-Time Gateway pushes the edit event immediately over the open connection. If offline, the event waits in that device’s durable queue and a lightweight “silent” push notification may be sent to encourage reconnection, but delivery of the actual edit content happens through the Sync Service on next connect — not through the push payload itself, since push payloads are size-limited and not delivery-guaranteed.
3.4 Applying a Delete
Delete follows the same event-sourced pattern but the result is a tombstone rather than new content: a MessageDeleted event is written, referencing the original message ID. The materialized state store replaces the message’s renderable content with a deletion marker (e.g., “This message was deleted”), while the event log — and often a separate, access-restricted audit log — retains the original content for compliance, moderation, and abuse-investigation purposes, even though normal clients will never render it again. This distinction between “gone from the log” and “gone from every UI” is important and frequently misunderstood: a true “delete from all storage everywhere” is a much stronger, harder guarantee (and one with real privacy and compliance implications) than “no longer shown to users,” and most platforms deliberately implement the latter while retaining the former internally for a bounded retention period.
3.5 Snapshotting — Bounding the Cost of Deep History
Pure event replay is elegant but does not scale forever: a device offline for a year in a conversation with millions of historical events would face an enormous, slow catch-up. The standard fix is snapshotting — periodically (e.g., after every N events, or on a time-based schedule) the Message Service materializes a full “current state as of sequence number K” snapshot and persists it. A device reconnecting after a long absence fetches the most recent snapshot at or before its cursor, then replays only the small tail of events between the snapshot point and the present — turning an unbounded replay into a bounded one. Snapshots do not replace the event log (which remains the durable source of truth and audit trail); they are a pure performance optimization layered on top, and can even be regenerated from the log if ever lost or corrupted.
3.6 Multi-Device Complications
Modern messaging accounts routinely span multiple devices — phone, tablet, desktop client, web client — all logged into the same account and expected to show an identical, converged view. This means the recipient set for fan-out is not “one device per user,” it is “every registered device per user,” and the sync / cursor machinery described above applies not just across different users in a conversation but across a single user’s own device fleet. A subtlety worth naming: if a user edits a message from their laptop while their phone is offline, the phone must eventually see that edit through exactly the same sync mechanism as any other recipient — there is no privileged “the sender already knows” shortcut, because from the system’s perspective, the laptop and phone are just two more devices that need to converge.
- Why use sequence numbers instead of timestamps to order edits and deletes?
- Walk through what happens if a device receives a delete event before it has ever received the original send event.
- What is the difference between “deleted from the UI” and “deleted from storage,” and why do most platforms only implement the former by default?
- How does snapshotting change the worst-case cost of catching up a device that has been offline for a year?
- Why does the sender’s own other devices not get special-cased out of the normal sync / fan-out path?
Data Flow & Message Lifecycle
Let’s trace an edit end to end — from sender request through durable append, real-time push to online devices, and eventual catch-up for an offline device that only reconnects days later.
Figure 2 — A single edit propagates through durable append, live push to already-connected devices, queued waiting for offline devices, and cursor-driven replay when the offline device eventually reconnects — all through the same underlying event stream.
4.1 Lifecycle Stages
Mutation request
Sender submits send / edit / delete; Message Service validates ownership, timing rules, and payload constraints.
Durable append
The event is written to the append-only log with a conversation-scoped sequence number — this write is the point of no return; once acknowledged, the event will eventually reach every recipient.
Materialization
The current-state store is updated so live reads (e.g., opening the conversation for the first time) reflect the latest state without replaying full history on every read.
Real-time fan-out
Online recipients receive the event immediately over their persistent connection and update their cursor.
Offline queuing
For offline recipients, the event is durably retained (referenced by the log, not duplicated) and a wake-up push notification may be sent, but is not the delivery mechanism for the actual content.
Reconnect & sync
When a device reconnects, it presents its last known cursor; the Sync Service replays every event since that cursor, in strict order, so the device converges to the exact same state as every other recipient.
Cursor advancement
Once a device has successfully applied an event, it advances its stored cursor, so a subsequent reconnect never re-fetches already-applied events.
- Why is “durable append” described as “the point of no return” — what does that guarantee actually buy the system?
- What would go wrong if a device’s cursor was updated before an event was fully applied to local storage, and the app crashed in between?
- How do you avoid a reconnecting device replaying years of history if it has been offline a very long time?
Advantages, Disadvantages & Trade-offs
Every design here is really a decision about where you spend complexity budget: storage cost vs. replayability, in-memory speed vs. audit trail, delete strength vs. compliance flexibility. Making those explicit turns the design into a defensible engineering choice.
Advantages of this architecture
- Event sourcing gives every device — no matter how stale — a deterministic, replayable path to the correct current state.
- Separating real-time push from offline sync means online and offline recipients share one correctness mechanism, not two divergent code paths.
- Full version history supports product features beyond the base requirement: “edited” indicators, edit history views, and abuse investigation.
- Sequence-based ordering avoids the pitfalls of relying on unsynchronized client or server clocks.
Disadvantages & costs
- Storage grows with every mutation, not just every message — a heavily-edited conversation costs more to store than a static one.
- Per-device cursor tracking adds real operational and storage overhead at scale (one cursor row per device per conversation, or an efficient equivalent).
- Long-offline devices can face a large catch-up replay, which needs careful engineering (snapshotting) to stay fast.
- True data erasure (for legal / compliance delete requests) is more complex in an append-only model and needs a deliberate purge mechanism layered on top.
5.1 Key Trade-offs
| Trade-off | Option A | Option B | What to consider |
|---|---|---|---|
| Storage model | Append-only event log | In-place row mutation | Event log gives replayability and audit trail at the cost of storage growth; in-place mutation is cheaper to store but loses history and makes offline sync far harder to reason about correctly. |
| Delete semantics | Soft delete (tombstone, content retained internally) | Hard delete (content purged everywhere) | Soft delete supports moderation / compliance and simpler sync logic; hard delete satisfies stronger privacy requirements but complicates replay and requires explicit purge propagation. |
| Catch-up strategy | Full event replay since cursor | Periodic state snapshot + replay only recent tail | Pure replay is simple but slow for very stale devices; snapshotting adds complexity but bounds worst-case catch-up time. |
| Edit window | Unlimited (Slack / Telegram-style) | Time-boxed (iMessage-style, e.g., 15 minutes) | Unlimited editing suits durable, searchable records; time-boxed editing limits how long recipients must tolerate content changing underneath them, which matters more for ephemeral chat products. |
- Why is a soft-delete tombstone usually preferred over immediately purging content everywhere?
- What real product and legal reasons would push you toward supporting true hard deletion in addition to soft delete?
Performance & Scalability
Assume a platform with 500 million daily active users, averaging 40 messages sent per user per day, with roughly 5% of sent messages later edited and 2% deleted.
500,000,000 users × 40 messages/day = 20,000,000,000 messages/day (sends).
Edits: 20B × 5% = 1,000,000,000 edit events/day.
Deletes: 20B × 2% = 400,000,000 delete events/day.
Total mutation events/day ≈ 21.4 billion, or roughly ~248,000 events/second sustained average, with typical daily peaks 5 to 8 times average during high-usage hours.
6.1 Fan-Out Amplification
A single mutation event does not cost one unit of work — it costs one unit of work per recipient device. A message edited in a 200-person group chat, where the average user has 1.5 active devices, generates roughly 300 individual delivery jobs from a single edit event. This fan-out multiplier is the dominant scaling factor for the Fan-Out Service, and group conversations (not 1:1 chats) are almost always the long tail that drives peak load, since a single popular group can have thousands of members.
6.2 Where the Bottlenecks Actually Are
Sequence number assignment
If every mutation in a conversation must go through a single ordering authority, that authority can become a hotspot for extremely high-traffic group conversations — mitigated by partitioning ordering authority per conversation shard rather than globally.
Cursor store writes
Every device, on every successfully applied event, ideally advances its cursor — at billion-message scale, batching cursor updates (advance once per received batch, not once per individual event) meaningfully reduces write load.
Cold catch-up replay
A device offline for weeks replaying a high-volume group chat’s entire backlog can be slow and resource-intensive — snapshotting (materializing current state as of a point in time) bounds this cost.
Real-time connections
Maintaining persistent connections for hundreds of millions of concurrent online devices requires horizontally sharded, stateful connection servers with careful connection-to-server affinity.
6.3 Scaling Techniques
Conversation-based partitioning
Partition the event log, ordering authority, and materialized state by conversation ID, so no single conversation’s traffic contends with unrelated ones, and load spreads naturally across the fleet.
Bounded replay
Periodically materialize a conversation’s full current state, so catch-up for a very stale device is “apply latest snapshot, then replay only the tail since the snapshot” rather than replaying from the beginning of time.
Batched delivery
When fanning out to a large group, batch multiple recipients’ delivery jobs rather than issuing one write per recipient per event, reducing per-event overhead at high fan-out multipliers.
Cursor compaction
Represent a device’s cursor as a single scalar (last applied sequence number per conversation) rather than a set of individually tracked message states, keeping the cursor store compact even for users in thousands of conversations.
- Why does a single edit in a large group chat cost far more system resources than an edit in a 1:1 chat?
- How would you design snapshotting so a device offline for six months does not have a terrible catch-up experience?
- Why partition ordering authority per conversation instead of using one global sequence generator?
6.4 CAP Theorem and the Event Log
During a network partition, the event log’s per-conversation ordering authority faces the same choice as any distributed system: preserve availability by allowing writes on multiple isolated sides of the partition (risking a merge conflict on healing), or preserve strict ordering by refusing writes on the minority side until the partition heals. Because the entire correctness model here depends on a single, unambiguous total order per conversation, this is one of the places in the system where leaning CP (consistent over available) for the ordering authority specifically is usually the right call — a brief write unavailability for one specific conversation shard during a partition is a far more tolerable failure mode than two divergent, unmergeable histories for the same conversation that could later show different users irreconcilably different pasts. Contrast this with something like the cursor store, discussed later, which can safely lean AP.
6.5 Cost Considerations at Scale
Storage cost is driven primarily by three things: raw event volume (sends plus edits plus deletes), how long full event history is retained before being eligible for compaction into snapshots, and how many devices are actively maintaining cursors. A pragmatic cost lever many platforms use is tiered retention: keep full granular event history “hot” for a relatively short window (weeks), then compact older history into periodic snapshots plus a much smaller retained tail, trading a small amount of audit-trail granularity for a large reduction in long-term storage cost — while keeping the compliance-relevant retention window (for legal hold or abuse investigation) explicitly separate and typically longer-lived in a more access-restricted store.
High Availability & Reliability
The core reliability requirement here is unusually strict: every recipient must eventually converge on the identical final state, with no permanently divergent views, even across arbitrary network partitions, device outages, and server failures.
7.1 Reliability Techniques
Replicated event log
Every mutation is written to a replicated log (a Kafka-style or database-backed write-ahead log) before being acknowledged to the sender, so a server crash immediately after acceptance never loses the mutation.
At-least-once + idempotent apply
The Real-Time Gateway and Sync Service may redeliver the same event more than once (after a connection blip); each event carries a unique ID so the client can safely discard duplicates rather than double-applying an edit.
Cursor-based resumability
Because catch-up is driven by “everything since my cursor,” a client can safely reconnect after any failure — mid-sync crash, network drop — and simply resume from its last durably-advanced cursor with no special-case recovery logic.
Concurrent-edit tie-break
If two devices belonging to the same sender attempt to edit the same message concurrently (rare but possible with multi-device accounts), the system needs a deterministic tie-break rule — typically “last sequence number wins,” applied identically on every device, so no two devices can disagree about the outcome.
7.2 Failure Mode Table
| Failure | Impact | Mitigation |
|---|---|---|
| Event log shard unavailable | Mutations for affected conversations stall | Replica promotion, multi-AZ replication, bounded failover time |
| Real-Time Gateway crash | Online devices connected to that node temporarily lose live updates | Client auto-reconnect + cursor-based resync closes any gap seamlessly |
| Duplicate event delivery | Risk of double-applying an edit / delete on client | Idempotent client-side application keyed by event ID |
| Cursor store corruption / loss for a device | Device might re-replay from scratch or miss updates | Fail safe toward full resync (costly but correct) rather than silently skipping events |
- Why must event application on the client be idempotent, given the system’s delivery guarantees?
- If a device’s stored cursor is somehow lost or corrupted, what is the safe default behavior, and why?
- How do you resolve two devices belonging to the same user concurrently editing the same message?
7.3 Disaster Recovery for the Event Log
Because the event log is the irreplaceable source of truth — unlike the materialized state store or snapshots, which can always be regenerated by replaying it — it deserves the platform’s strongest durability guarantees: cross-region replication, regular backup verification (not just backup creation), and a tested, rehearsed regional failover procedure. A useful design discipline is to treat every other data store in this system (materialized state, cursor store, snapshots) as recoverable-by-construction from the event log, and to size your operational rigor accordingly: maximal care for the log itself, and comparatively lighter-weight recovery procedures for everything derived from it, since “rebuild it by replaying the log” is always available as a fallback.
7.4 Defining an Honest SLA
Similar to any distributed delivery system, it is worth being explicit about what is actually being promised. A defensible SLA here reads something like: “100% of accepted mutations are durably logged and will eventually be delivered to every recipient device once that device is online and syncs, with real-time delivery to already-connected devices within a low single-digit number of seconds under normal operating conditions.” This is a strictly stronger guarantee than the push-notification case discussed in related literature — because delivery here does not depend on an external, uncontrolled gateway’s best-effort final hop, the system can and should commit to eventual, guaranteed delivery, with only the timing of that delivery being variable based on the recipient device’s connectivity.
- Why is an honest SLA for this system able to promise eventual guaranteed delivery, unlike a push-notification-gateway-dependent system?
- Why does the event log deserve stronger durability investment than the derived stores built on top of it?
Security
Message mutations touch content that is intrinsically sensitive, so authorization, retention control, and tamper-evidence all have to be first-class concerns, not afterthoughts.
Authorization on every mutation
The Message Service must verify the requester actually owns the message being edited / deleted (or has moderator / admin rights in the conversation) before accepting the mutation — never trust a client-supplied “I own this message” claim.
End-to-end encryption implications
In E2E-encrypted platforms, an edit is itself a new encrypted payload; the server can route it without ever reading plaintext, but this means server-side validation of “is this a legitimate edit of that message” is necessarily limited, and trust shifts partly to the client to enforce edit rules correctly.
Retention & purge policy
Internally retained “deleted” content (kept for abuse investigation or legal hold) must be access-controlled far more tightly than normal message content, and subject to its own bounded retention window and audit trail of who accessed it and why.
Tamper-evidence in the event log
Because the event log is the source of truth for what a message “really” said at any point, it should be protected against unauthorized modification — an attacker who could rewrite history in the log could make a message appear to have said something it never did.
Rate limiting edits / deletes
Prevents abuse patterns like rapid edit-spam intended to bypass content moderation systems that scan messages at send time but might not equally scrutinize every subsequent edit.
8.1 Threat Modeling the Sync Path Specifically
Beyond authorization on individual mutations, it is worth threat-modeling the Sync API itself, since it is a high-value target: an attacker who could forge or manipulate cursor values might attempt to trigger excessive replay (a denial-of-service vector against the Sync Service) or, worse, attempt to fetch events for conversations they were never a member of by guessing or brute-forcing conversation identifiers. Defending against this means the Sync API must independently verify conversation membership for the requesting user on every call — never assuming that possessing a valid cursor implies valid access — and should rate-limit sync requests per device to blunt any attempt to use repeated full-history replay as an amplification or resource-exhaustion attack.
- In an end-to-end encrypted messaging platform, how much can the server actually validate about an edit request?
- Why is rate-limiting edits a moderation concern, not just an infrastructure-protection concern?
- Who should be allowed to read the internally-retained content of a deleted message, and how would you enforce that?
- Why must the Sync API independently re-verify conversation membership on every call rather than trusting a valid cursor?
8.2 Privacy-by-Design for Retained Content
Because deleted content that is internally retained for abuse investigation is, functionally, a liability if mishandled, it is worth designing its access path with the same rigor as any other sensitive data category: encryption at rest with keys scoped separately from normal message content, mandatory access logging so every read of retained deleted content is itself auditable, and an automatic, enforced expiry so retained content does not silently accumulate indefinitely beyond whatever policy justified keeping it in the first place. Treating this store as an afterthought bolted onto the main event log is a common early-stage mistake that becomes expensive to unwind once compliance or security review catches up with a growing platform.
Monitoring, Logging & Metrics
Convergence bugs are silent by nature — two devices quietly disagreeing about what a message says is exactly the class of failure users report late, if at all. Instrumentation has to be designed to surface that class of problem long before a support ticket arrives.
9.1 Key Metrics
Event log write latency
Time from mutation request to durable log append acknowledgment.
Fan-out completion latency
Time from log append to delivery confirmation for all currently-online recipients.
Sync replay volume & duration
How much backlog reconnecting devices typically replay, and how long it takes; a leading indicator if snapshotting is not keeping pace with mutation volume.
Cursor staleness distribution
How far behind, on average and at the tail, devices’ cursors are relative to the latest event, segmented by online / offline state.
Divergence detection rate
Any mechanism that periodically checksums a conversation’s materialized state across replicas / devices and flags mismatches, which should ideally be zero and any non-zero rate is a correctness bug, not just a performance blip.
9.2 Logging & Tracing
Every mutation event should carry a trace ID propagated from the original client request through log append, fan-out, and every downstream delivery attempt, so a support escalation like “my friend still sees my old message, I edited it an hour ago” can be root-caused precisely — was the edit delivered to their device, did their client fail to apply it, or is their cursor stuck?
- How would you detect, proactively, that two recipients’ views of a conversation have silently diverged, before a user reports it?
- What is your triage process for “I edited a message and my friend still sees the old version”?
Deployment & Cloud Architecture
The stateless and stateful tiers here have very different operational profiles, and treating them uniformly is the fastest way to break sync correctness during a routine deploy.
Message & Fan-Out Service
Stateless instances behind an autoscaler, scaled by mutation throughput and queue depth.
Real-Time Gateway tier
Requires sticky connection routing (a device’s WebSocket stays pinned to a specific gateway instance for the connection’s lifetime), typically deployed with consistent-hashing-based load balancing.
Conversation-aware regional routing
A conversation’s shard (and its ordering authority) lives close to where most of its participants are, reducing cross-region latency for the common case, while still supporting global reachability for cross-region participants.
Client-side event application rollout
Since correctness depends on every client version applying events identically, schema and behavior changes to how edits and deletes are represented need staged rollout and backward compatibility with older client versions still in the field.
- Why does the Real-Time Gateway tier need sticky routing while the Message Service does not?
- What backward-compatibility risks exist if you change the event format for edits, given that old client versions remain in the field for a long time?
Databases, Caching & Load Balancing
Different pieces of state in this system have wildly different durability and read-pattern requirements, and matching each to the right store is what keeps costs bounded and behavior predictable.
11.1 Event Log Storage
The event log favors an append-optimized, horizontally partitioned store — a distributed log system (Kafka-like) or a database explicitly designed for ordered, immutable event streams, partitioned by conversation ID for both write scalability and to keep a conversation’s full ordered history colocated for fast replay.
11.2 Materialized State Store
The current-state store is read-optimized, typically a key-value or document store keyed by message ID (or conversation ID + message ID), rebuilt incrementally as events are applied — this is what powers the fast “open a conversation and see current content instantly” experience without replaying history on every read.
11.3 Cursor Store
A high-write-throughput key-value store keyed by (device ID, conversation ID), storing just the last-applied sequence number. Given this is written extremely frequently (every device, every event, potentially batched), it benefits from a store optimized for high write QPS with relaxed durability-per-write requirements — losing the very latest cursor advance in a rare crash just means a small amount of redundant replay next reconnect, not a correctness problem.
11.4 Caching
Hot conversation cache
Materialized state for actively-used conversations cached in memory to avoid repeated storage reads during bursts of activity.
Snapshot cache
Recent snapshots cached so catch-up sync for moderately-stale devices avoids a storage round-trip for the base state.
11.5 Load Balancing
Layer-7 balancing for stateless API traffic; consistent-hashing-based routing for the stateful Real-Time Gateway tier, ensuring even connection distribution while preserving the sticky affinity each connection needs for its lifetime.
- Why can the cursor store tolerate relaxed write durability while the event log cannot?
- How would you design the materialized state store’s schema to make “apply this edit event” a cheap, targeted update rather than a full re-read of history?
APIs & Microservices Design
The public surface is deliberately narrow: mutations in, sync out, and a small set of specialized internal channels for presence, moderation, and audit.
Message Mutation API
Accepts send / edit / delete requests, returns immediately once durably logged (async fan-out), not once every recipient has received it.
Sync API
Accepts a device’s last known cursor per conversation and returns the ordered set of events since then, used both on reconnect and periodically as a safety-net reconciliation even for devices that believe they are online and current.
Presence API
Exposes which recipients are currently online, informing the Fan-Out Service’s choice between real-time push and offline queuing.
Moderation / Audit API
Internal-only, tightly access-controlled, exposing the full mutation history and originally-deleted content for abuse investigation and legal compliance workflows.
These services communicate primarily through the event log itself, which doubles as both the durability mechanism and the primary internal message bus — a design that reduces the number of distinct integration points other services need to reason about.
- Why should the Mutation API return before fan-out to all recipients completes?
- Why run a periodic reconciliation sync even for devices that are currently connected and believe they are up to date?
Design Patterns & Anti-patterns
The whole system is essentially a careful stack of well-understood patterns — and a matching set of anti-patterns it deliberately refuses to use.
13.1 Patterns That Work
Event sourcing
The foundational pattern that makes deterministic, replayable convergence possible for arbitrarily stale clients.
Cursor-based checkpoint sync
Lets any client resume exactly where it left off without special-casing “how long have you been offline.”
Idempotent, ID-keyed apply
Makes at-least-once delivery safe under retries and duplicate delivery.
Snapshot + tail replay
Bounds worst-case catch-up cost for very stale clients without giving up the correctness benefits of full event history.
Tombstone-based soft delete
Lets “deleted” propagate through the exact same mechanism as any other mutation, rather than requiring special-case deletion logic.
13.2 Anti-patterns to Avoid
- Mutating message rows in place: overwriting a message’s content directly on edit destroys history, makes offline sync effectively impossible to get right (a device that missed the original send has nothing coherent to reconcile), and eliminates any audit trail.
- Using wall-clock timestamps for ordering: clock skew across distributed servers can cause an edit to appear to have happened before the original send from another server’s perspective, corrupting the reconstructed history.
- Treating push notification payloads as the delivery mechanism for edit / delete content: push payloads are size-limited, not delivery-guaranteed, and can be dropped by the OS — actual content must flow through the durable Sync path, with push used only to encourage the app to wake up and sync.
- Assuming a single global sequence counter scales: a single, unsharded ordering authority for all conversations platform-wide quickly becomes a severe bottleneck and single point of failure at scale.
- What specifically breaks in offline sync if you mutate message content in place rather than event-sourcing it?
- Why can you not rely on push notification payloads to actually carry the edited content?
Best Practices & Common Mistakes
The disciplines below are the difference between a design that works in the demo and a design that keeps working at billion-message scale, three years and many client versions later.
Do
- Make client-side event application idempotent and keyed by a unique event ID from day one — retrofitting this after duplicate-delivery bugs surface in production is far more painful.
- Run periodic background reconciliation syncs even for “healthy,” currently-connected clients, as a safety net against silent state drift from bugs elsewhere in the pipeline.
- Design the event schema to be forward-compatible from the start, since old client versions will remain in the field long after new mutation types (edit-with-attachment-change, reactions, threads, etc.) are introduced.
- Clearly separate “hidden from UI” soft-delete from “purged from all storage” hard-delete in both your data model and your product / legal documentation, since conflating them creates real compliance risk.
Common mistakes
- Underestimating fan-out amplification in large group conversations, leading to Fan-Out Service capacity that looks fine in 1:1-chat-dominated load tests but falls over under real group-chat-heavy production traffic.
- Not bounding catch-up replay cost, so a small number of very-long-offline devices (weeks or months) generate disproportionately expensive sync operations that degrade service for everyone sharing that infrastructure.
- Forgetting that multi-device accounts need the same convergence guarantees across a single user’s own devices, not just across different users — a user editing on their phone should see that reflected on their laptop just as reliably as a friend does.
- Why is multi-device convergence for a single user’s own account just as hard as cross-user convergence?
- What load-testing gap commonly causes teams to be surprised by group-chat fan-out costs in production?
14.1 Testing Convergence, Not Just Delivery
Standard integration tests tend to check “was the message delivered,” which is necessary but not sufficient here — the harder, more valuable test is “do N independently-simulated devices, each following a different, randomized pattern of online / offline transitions and reconnect timing, all converge on byte-identical final state after a sequence of interleaved sends, edits, and deletes.” Building this kind of property-based, randomized convergence test early — effectively a chaos test for the sync protocol itself — catches an entire class of subtle ordering and idempotency bugs that conventional request / response testing simply cannot reach, since those bugs only manifest from specific, unusual interleavings of concurrent mutations and device connectivity states.
- Why is “was the message delivered” an insufficient test, and what would a stronger convergence test look like?
- What class of bugs does randomized, interleaved online / offline simulation testing catch that normal integration tests miss?
Real-World Industry Examples
Very different products, very similar underlying architecture — the mutation-consistency problem is the same regardless of whether the surface UX is ephemeral chat or a permanent team knowledge base.
Delete for Everyone
“Delete for Everyone” is a widely recognized real-world instance of exactly this problem — a tombstone-style delete that must propagate to every recipient, including those offline at the moment of deletion, while operating within an end-to-end encrypted transport that limits server-side visibility into content.
Unlimited edit & delete
Supports unlimited-time editing and deletion, illustrating a product decision to treat messages as fully mutable indefinitely, which places correspondingly higher demands on long-term event history retention and sync correctness for very old messages.
Durable, searchable record
Messages function as a durable, searchable knowledge record for teams, so edit history and “(edited)” indicators are core, expected functionality — Slack’s architecture leans heavily on the idea that a channel’s history must be fully reconstructible and consistent across every member’s client, including ones that have not been opened in months.
Time-boxed edit window
Time-boxes editing to a short window and shows edit history to recipients, a deliberate product trade-off that limits how long the “convergence” problem needs to remain open per message, simplifying some of the offline-catch-up cost compared to unlimited-editing platforms.
Editable messages in large public channels
Supports message editing extensively in persistent, often very large public / community channels, which stresses the fan-out amplification problem discussed earlier at a different scale than private messaging platforms — a single edit in a large server can affect an enormous concurrent audience.
15.1 A Cross-Platform Pattern, Not a Single Vendor’s Trick
What is instructive across these five examples is that none of them arrived at event-sourced, cursor-based sync because it was fashionable — each was independently pushed toward it by the same underlying constraint: users expect a conversation to look identical no matter which device or how much time has passed since they last opened the app. That constraint does not change based on whether the product optimizes for ephemeral personal chat (WhatsApp), permanent searchable records (Slack), or large public communities (Discord) — only the product-level policy choices layered on top (edit windows, retention periods, who can moderate) differ. This is a useful lens for an interview: when asked to design “a messaging feature,” look past the product-specific UI decisions to the underlying convergence guarantee being asked for, since that is almost always the harder and more transferable engineering problem.
- How does end-to-end encryption change what WhatsApp’s servers can and cannot verify about a “delete for everyone” request?
- Why might Discord’s architecture need to handle fan-out amplification differently than a primarily 1:1-messaging platform?
- Why do such different products (Slack, Discord, WhatsApp) converge on such similar underlying sync architectures?
Frequently Asked Questions
The questions that come up most often on this design — and the crisp answers that show a clear understanding of where the real correctness guarantees live.
What happens if a recipient’s device is offline for months — will it still eventually see all the edits and deletes?
Yes, as long as the event log (or its snapshotted equivalent) retains history back to that device’s last cursor position. Most platforms set a practical retention / snapshot policy so extremely stale devices fall back to “resync full current state” rather than replaying an unbounded event history, but the end result — correct final content — is still guaranteed.
Can two people ever see permanently different versions of the same message?
Not by design — the system is built specifically to prevent permanent divergence. Temporary divergence (one device has not synced yet) is expected and normal; the guarantee is eventual convergence to an identical final state, not instantaneous simultaneous updates.
Does editing a message change its original timestamp or position in the conversation?
No — the message retains its original sequence position (when it was originally sent); only its content and an “edited” marker change. This is a deliberate product and technical choice: reordering a message on edit would be confusing to users and would complicate the ordering guarantees the whole system depends on.
How is this different from general event sourcing / CQRS used in other domains?
It is the same underlying pattern (immutable event log plus a derived read-optimized view), applied to a domain with an unusually strict multi-party convergence requirement — many independent, often long-offline readers must all reconstruct the identical state, which pushes harder on cursor-based sync and snapshotting than a typical single-service CQRS use case.
What happens to a delete request for a message the recipient has already screenshotted or forwarded?
The system has no ability to control content once it has left the platform’s own storage and rendering pipeline — deletion only guarantees the message is removed from the platform’s UI and (per retention policy) storage going forward; it cannot retroactively affect copies made outside the system.
How do you handle an edit to a message that was already deleted before the edit event arrives?
Because every mutation references the original message’s ID and carries its own sequence number, a client applying events in order will always see the delete event and correctly render the tombstone, regardless of what order the edit and delete were physically delivered in — the deterministic rule is simply “apply events in sequence-number order, and the last applicable state wins,” so a delete that logically happened after an edit always takes precedence, and vice versa.
Does this architecture work the same way for group conversations with thousands of members as it does for 1:1 chats?
The core mechanism is identical — one event log, one ordering scheme, one sync protocol — but the operational profile differs significantly, since a single mutation in a large group generates a proportionally larger fan-out workload. Most production systems apply the same architecture uniformly but provision Fan-Out Service capacity, and sometimes batching strategies, specifically account for the long tail of very large groups.
Summary & Key Takeaways
A compact recap of the whole design, and the mental model worth carrying into any future system where many independent, sometimes-offline observers must converge on a shared, mutable truth.
The seven ideas worth remembering
- Treat messages as an event-sourced entity — original send, edits, and deletes are all immutable events, never in-place mutations — because this is what makes correct offline catch-up possible at all.
- Order mutations with per-conversation, server-assigned sequence numbers, not wall-clock timestamps, to avoid clock-skew-induced corruption of history.
- Give every device a durable cursor and a Sync API that answers “what changed since cursor X” — this single mechanism handles reconnect-after-five-minutes and reconnect-after-five-months identically.
- Push notifications are a wake-up signal, never the delivery mechanism for actual edit / delete content — the durable Sync path is the only guaranteed path.
- Fan-out amplification in large group conversations, not raw message volume, is usually the real scaling bottleneck — plan capacity accordingly.
- Separate soft-delete (hidden from UI, retained internally) from hard-delete (fully purged) deliberately, both in your data model and in what you promise users and regulators.
- Idempotent, ID-keyed event application on the client is what makes at-least-once delivery safe — build it in from day one rather than retrofitting it after duplicate-delivery incidents.
The recurring theme across this entire design is that “consistency for offline users” is not a special feature bolted onto a messaging system — it is what falls out naturally once you commit to treating every message mutation as a durable, ordered, replayable event rather than a database row you overwrite. Once that commitment is made, online delivery, offline catch-up, multi-device sync, and even audit / compliance requirements all become variations of the same underlying mechanism, rather than separate problems each requiring their own bespoke solution.
For an interview setting specifically, the strongest signal you can give is walking through why each piece exists in terms of the failure mode it prevents — sequence numbers exist because clocks skew, cursors exist because “since when” needs a precise answer, snapshots exist because unbounded replay does not scale, and idempotent application exists because at-least-once delivery is the only realistic guarantee a distributed system can make. A candidate who can trace each component back to the specific correctness or scalability problem it solves demonstrates a genuinely deeper understanding than one who can only describe the boxes and arrows in isolation.