Two people are typing in the same chat and one is on a train. What arrives, in what order, and what do you store?
Chat ordering with offline clients separates ephemeral typing indicators from durable messages. Messages need a server-assigned per-conversation sequence and a client-generated id for deduplication, so reconnecting phones replay outboxes without duplicates.
What the interviewer is scoring
- Does the candidate split ephemeral typing state from durable messages before designing either
- Whether ordering is assigned by the server per conversation rather than taken from a client clock
- That the reconnecting client's outbox is deduplicated by an id the client generated, not by content
- Can they say where a late message is placed in the transcript and defend the choice against the alternative
- Whether read state is modelled as a per-participant cursor instead of a flag on every message
Answer
Short answer
For chat ordering with offline clients, never store typing indicators; treat them as expiring hints. Store messages with a server-assigned sequence per conversation for transcript order and a client-generated id for retry deduplication. A reconnecting phone replays its outbox, the server returns existing sequence numbers for duplicates, and clients sync from their last seen sequence.
Two data models wearing the same coat
One is on a train. The other is at a desk watching three dots appear and disappear. Those dots and the message that follows them look like the same feature and they are not, and saying so is the first thing worth marks.
A typing indicator is a hint with a lifetime of a few seconds. It has no history, nobody audits it, and losing one costs nothing. A message is a durable fact that must survive the phone, the server that received it and the year. Every design choice differs between the two, so decide which you are talking about before you talk about storage or ordering.
Typing gets no storage at all. The sender emits an event when they start, at most once every few seconds while they continue, and the recipient's client hides the dots on a short timer of its own rather than waiting for a stop event. That timer is the point: a phone that dies mid-sentence never sends "stopped typing", and if you depend on that event the dots stay up forever. Same lesson as any presence signal. Expire it locally, do not wait to be told.
The server owns the order
Client clocks are wrong. Some are minutes out, some are in the wrong time zone by user error, and a few are deliberately set forward by someone who worked out that it floats their messages to the top. So the transcript cannot be sorted by a timestamp the client supplies.
Give each conversation a monotonic sequence and assign it when the message is accepted. The message row then carries two times and one order: the sequence, the time the sender says they composed it, and the time you received it. Order the transcript by sequence. Display the composed time. Keep the received time for support tickets, because the gap between the two is the entire explanation of every "why did this arrive late" complaint.
A per-conversation counter also gives you cheap paging, cheap gap detection and cheap sync. A client that holds up to sequence 812 asks for everything after 812 and knows it is whole when there are no holes. Compare that with a client that has to ask "what changed since this timestamp" and cannot tell a missing message from a slow one.
What the train actually does to you
The phone in the tunnel keeps accepting typed messages. It has to, or the product is unusable on a commute. So each composed message is written to a local outbox with a client-generated id, and the id is what makes the retry safe.
On reconnect the client sends the outbox. Some of those messages may already have reached the server before the radio dropped, with the acknowledgement lost on the way back. The server therefore treats the client id as a uniqueness constraint per conversation: first write wins, a repeat returns the sequence number already assigned rather than creating a second row. Without that id you are deduplicating by comparing text, which breaks the moment somebody sends "ok" twice on purpose.
sequenceDiagram
participant T as Phone in tunnel
participant S as Chat service
participant D as Desk client
T->>S: send msg cid-7
Note over T,S: ack lost, radio drops
D->>S: send msg cid-9
S-->>D: assigned seq 41
T->>S: reconnect, replay cid-7
S-->>T: already stored as seq 40
S-->>D: backfill seq 40 after seq 41The interesting moment in that exchange is the last line. Sequence 40 was assigned before 41, so it belongs above it in the transcript, but the desk client had already drawn 41 and read it. Redrawing history under a reader who has moved on is its own defect.
Where the late message goes, and why it is a judgement call
Two defensible answers, and an interviewer mostly wants to see that you know it is a choice.
Insert by sequence, and the transcript is internally consistent: cause precedes effect, and two clients that sync fully agree. The cost is that a message can appear above the line a user has already read, so it is never seen. Append at the point of arrival, and nothing is missed, but the transcript now shows an answer above the question it answered.
Most chat products insert by sequence and mitigate the visibility problem rather than avoiding it: an unread marker that stays anchored where the reader left off, and a timestamp on the late message that shows its composed time so the ordering makes sense to a human. If the delay was long, some products label it. That is a product decision, not a storage one, and the storage supports either because both times are on the row.
A strong candidate says it plainly: "The order is the server's, the timestamp is the sender's, and the reader's position is a third thing that neither of them controls."
What you store, concretely
One row per message, keyed by conversation and sequence, holding the sender, the body, the client id, the composed time and the received time. Nothing about typing. Nothing about who has read it.
Read and delivery state belong in a separate small structure: one cursor per participant per conversation, holding the highest sequence they have seen. That is a handful of bytes per person per conversation, updated as they scroll. The alternative is a row per message per recipient, which in a group of fifty turns one message into fifty state rows and turns a read receipt into a fan-out problem. Cursors also answer the unread count with arithmetic instead of a query: highest sequence minus my cursor.
Attachments are references, not payloads. The row holds a pointer into blob storage plus enough metadata to render a placeholder while the bytes arrive, which is what makes a transcript scroll smoothly over a slow connection.
The mistake that survives review
Sorting by a client timestamp is the obvious error and it is usually caught. The subtler one is treating the typing indicator as a message: putting it on the same durable path, giving it retries, and storing it so that "was typing" can be queried later. It works in development. In production it is a write for every few keystrokes from every active user, on the path that must never be slow, in service of a signal that expires before anyone could read it.
The other one worth naming is acknowledging a message to the sender before it is durable. The single tick means "the server has it", and if the server has it only in memory, a restart loses a message the sender has been told was sent. Acknowledge after the write commits, and let the client show a pending state until then.
The order of a conversation is a property the server assigns, deduplication is a property the client's id provides, and read position belongs to neither of them — three separate concerns that a single timestamp column quietly conflates.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- The two participants send messages in the same millisecond to different regional servers. What decides which sequence number each gets?
- A user edits a message that a recipient has already read. What do you store, and what does the recipient see?
- How would you show "delivered" without writing a row per message per recipient?
- A client's outbox holds forty queued messages after a long flight. What order do you send them in, and what do you do if the tenth is rejected?
Related questions
- Usage records arrive duplicated, late and out of order, and the tariff changed in the middle of the month. How does mediation and rating cope?hardAlso on deduplication6 min
- A driver spends part of the day with no mobile signal. How do you design the app so the round still works?hardAlso on offline-first5 min
- How do you leverage WorkManager to guarantee execution, handle conflict resolution, and optimise battery usage in an offline-first Android application requiring bidirectional synchronisation?hardAlso on offline-first3 min
- Every message must survive the phone being wiped, and a user has 40,000 of them. Where do they live and how are they paged?hardAlso on chat6 min