Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
The first version of every chat backend orders messages by created_at. It works on one machine, in development, with one user. It fails in production in a way that is genuinely hard to debug, because the failure is invisible at the moment it happens and only appears later when someone scrolls. Two servers with clocks 60 milliseconds apart will hand you a reply that sorts above the question it answers. NTP does not fix this; NTP keeps clocks within tens of milliseconds of each other, and chat messages are routinely sent within tens of milliseconds of each other. Worse, wall clocks go backwards — a correction, a VM migration, a leap-second smear — and a message written during that window will sort into the past forever. And time is not unique: two messages in the same millisecond need a deterministic tie-break, or your pagination cursor will skip one message and repeat another every time the page boundary lands on the tie. The fix is to stop asking what time it was and start asking what position this message occupies, which is a different question with a different, much stronger answer. This challenge implements the answer three ways, because real systems use all three for different jobs.
Three artefacts, three jobs, and the mistake is using one for another’s job.
A per-channel sequence number is the ordering. A single owner for each channel hands out strictly increasing integers, so the order of a channel is defined by arrival at that owner — no clock is consulted at any point. This is also why the natural unit of ownership in chat is the channel, not the user or the server: channels are independent, so a million channels can be sequenced by a hundred nodes with zero coordination between them. The cost is that a channel has exactly one writer at a time, which is precisely the constraint that challenge 8 stress-tests.
A snowflake ID is the identity. It is globally unique with no coordination and roughly time-sortable, which makes it a good primary key, a good idempotency handle and a good thing to put in a URL. It is not an order: two nodes minting IDs in the same millisecond break ties arbitrarily. Treating a snowflake as a channel order is the same bug as created_at, just harder to spot because the IDs look sorted.
A hybrid logical clock is the fallback when there is genuinely no single owner — merging two regions after a partition, or ordering events across different channels. It carries a wall-clock component so a human reading the number can tell roughly when something happened, and a logical counter that guarantees it never repeats and never goes backwards even when the underlying clock does.
The fourth tab is the client-side consequence: because the transport can deliver out of order (a reconnect backfill racing a live push), the client renders from a reorder buffer keyed on seq, never straight off the socket.
// THE ORDERING — per-channel sequence with leased blocks.
// One owner per channel. The durable store is touched once per BLOCK,
// not once per message, so a busy channel costs ~1 round trip per 500
// sends instead of one per send.
package seq
import (
"context"
"errors"
"sync"
)
const blockSize = 500
var ErrLostOwnership = errors.New("seq: fenced by a newer owner")
type Store interface {
// ReserveBlock atomically bumps the persisted high-water mark for a
// channel by n and returns the first seq of the reserved block.
// It MUST reject the call if fence is older than the fence it has
// recorded for this channel — that check is the split-brain guard
// (challenge 8).
ReserveBlock(ctx context.Context, channel string, n int64, fence uint64) (int64, error)
}
type Allocator struct {
mu sync.Mutex
store Store
fence uint64 // ownership token, monotonic, from the lease service
next map[string]int64
end map[string]int64
}
func New(store Store, fence uint64) *Allocator {
return &Allocator{
store: store, fence: fence,
next: map[string]int64{}, end: map[string]int64{},
}
}
func (a *Allocator) Next(ctx context.Context, channel string) (int64, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.next[channel] >= a.end[channel] {
start, err := a.store.ReserveBlock(ctx, channel, blockSize, a.fence)
if err != nil {
// On ErrLostOwnership the correct move is to REFUSE sends for
// this channel, not to guess a number. See challenge 8.
return 0, err
}
a.next[channel] = start
a.end[channel] = start + blockSize
}
s := a.next[channel]
a.next[channel] = s + 1
return s, nil
}
// PROPERTY, and it is the one people get wrong:
// this produces a STRICTLY INCREASING sequence, not a DENSE one. A
// crash abandons the rest of a block, leaving a hole. Holes are fine
// for ordering and for cursors ("give me seq > 41220"). They are fatal
// if any client treats seq as a message COUNT — so unread counts are
// derived from cursors, never from arithmetic on seq (challenge 6).go run main.go