<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MANGESH MANDLIK</title>
    <description>The latest articles on DEV Community by MANGESH MANDLIK (@mangeshmandlik).</description>
    <link>https://dev.to/mangeshmandlik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4131208%2Fa9ac2096-0793-425e-8cf2-63f73f694fe7.jpg</url>
      <title>DEV Community: MANGESH MANDLIK</title>
      <link>https://dev.to/mangeshmandlik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mangeshmandlik"/>
    <language>en</language>
    <item>
      <title>Google Docs System Design: Real-Time Editing, OT, CRDT &amp; Scale</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Wed, 28 Oct 2026 03:26:57 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/google-docs-system-design-real-time-editing-ot-crdt-scale-2d26</link>
      <guid>https://dev.to/mangeshmandlik/google-docs-system-design-real-time-editing-ot-crdt-scale-2d26</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwm0m1j983a42reo499g0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwm0m1j983a42reo499g0.png" alt="google docs final architecture" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two people open the same document. Alice inserts a sentence near the top. Bob deletes a word in the same paragraph, before Alice's change reaches his laptop. Both see their own edits immediately. A few moments later, both should see the &lt;strong&gt;same document&lt;/strong&gt;, without silently losing either edit.&lt;/p&gt;

&lt;p&gt;That sounds like a text editor with WebSockets. It is actually a distributed synchronization problem.&lt;/p&gt;

&lt;p&gt;In this guide, we'll design a &lt;strong&gt;Google Docs-style real-time collaborative editor&lt;/strong&gt; from a single-user autosave application to a production-oriented architecture. We'll examine concurrent edits, Operational Transform (OT), CRDTs, authoritative document ownership, durability, offline recovery, presence, version history, and scaling. This is an illustrative system design—not a description of Google's private implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What are we designing?
&lt;/h2&gt;

&lt;p&gt;Users should be able to create and open documents, edit simultaneously, see other collaborators' changes and cursors, reconnect after losing connectivity, review earlier versions, and share documents with appropriate permissions.&lt;/p&gt;

&lt;p&gt;The essential engineering requirements are distinct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local responsiveness:&lt;/strong&gt; typing should appear immediately, even when the network is slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convergence:&lt;/strong&gt; healthy clients that have processed the same accepted history should arrive at the same document state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability:&lt;/strong&gt; an edit reported as &lt;em&gt;saved&lt;/em&gt; must survive failures covered by the stated durability policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization:&lt;/strong&gt; a client must not continue reading or writing after access is revoked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; millions of open connections and occasional extremely popular documents must not overload one global server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Convergence, durability, and preserving a user's intended meaning are &lt;strong&gt;not interchangeable guarantees&lt;/strong&gt;. A synchronization algorithm may converge perfectly while combining two semantically conflicting edits into awkward text.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Start simple: one user, one document
&lt;/h2&gt;

&lt;p&gt;For a single-user editor, the client can maintain a local buffer and periodically save the latest body to storage:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8ovbzq63l6538zkdypz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8ovbzq63l6538zkdypz.png" alt="single user" width="800" height="315"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Editor → Autosave API → Document database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks under concurrent writers. Suppose both Alice and Bob read version 10. Alice saves a modified body as version 11. Bob then saves his independently modified version 10 body. If the server simply overwrites the entire document, Bob's save can erase Alice's change.&lt;/p&gt;

&lt;p&gt;A conditional write that rejects Bob's stale version prevents silent overwrite, but still does not provide a good collaborative experience: Bob needs to reconcile his edits with Alice's. We therefore need &lt;strong&gt;operation-based synchronization&lt;/strong&gt;, not blind whole-document replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Evolve to real-time collaboration
&lt;/h2&gt;

&lt;p&gt;The client applies edits optimistically, then sends operations to a backend over a persistent WebSocket connection. A document owner reconciles concurrent operations and assigns authoritative versions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         ┌───────────────────────┐
Editor A ── WebSocket ────┤                       │
                         │   WebSocket Gateways  │
Editor B ── WebSocket ────┤  sessions, auth, I/O  │
                         └──────────┬────────────┘
                                    │
                              Shard Router
                                    │
                         ┌──────────▼────────────┐
                         │ Document Owner        │
                         │ validate + dedupe     │
                         │ OT / merge + version  │
                         └──────────┬────────────┘
                                    │
                         Durable Operation Log
                                    │
                         Commit acknowledgement
                                    │
                            Broadcast Layer
                                    │
                           Other subscribers

Separate supporting services:
  Presence / cursors     Snapshots / history
  Offline resync         Permission changes
  Ownership coordination Monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F50i1bfkmj4mvzwi3jk8w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F50i1bfkmj4mvzwi3jk8w.png" alt="websocket architecture" width="799" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why WebSockets?&lt;/strong&gt; Edits, acknowledgements, cursors, and presence updates are small and bidirectional. A persistent connection avoids repeated polling overhead and supports low-latency delivery. HTTP remains useful for document creation, initial snapshot retrieval, sharing, and history browsing. WebSockets provide transport—not correctness, ordering, or durable storage by themselves.&lt;/p&gt;

&lt;p&gt;The gateway authenticates sessions, applies message-size and rate limits, and routes traffic. It must &lt;strong&gt;not&lt;/strong&gt; be the source of document truth: a gateway can fail while the document's committed history remains intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Define the operation protocol before discussing OT
&lt;/h2&gt;

&lt;p&gt;Instead of sending the entire document after every keystroke, a client sends a logical operation. A simplified message might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_op"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"doc_842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"session_19"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"clientSeq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;391&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"opId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"op_9012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"baseVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"insert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;" collaborative"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each field solves a different problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docId&lt;/code&gt; routes the operation to the correct document.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opId&lt;/code&gt; identifies a logical edit across retries, allowing idempotent deduplication.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessionId&lt;/code&gt; and &lt;code&gt;clientSeq&lt;/code&gt; help identify gaps and ordering within a client session.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;baseVersion&lt;/code&gt; tells the server which authoritative document version the operation was prepared against.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;operation&lt;/code&gt; describes the intended mutation, using a schema understood by the synchronization engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server obtains its authoritative ownership epoch from trusted routing/coordination state; it must &lt;strong&gt;not trust an arbitrary client-supplied epoch&lt;/strong&gt; as proof of ownership. The durable writer checks the owner's fencing token.&lt;/p&gt;

&lt;p&gt;For a rich-text editor, the operation model also needs structural semantics: paragraphs, formatting spans, lists, tables, and embedded objects. A plain string index is useful for explaining concurrency, but is not a complete rich-document model. Text-offset conventions (for example, Unicode code points versus UTF-16 units) must be consistent across clients and server.&lt;/p&gt;

&lt;p&gt;A committed response might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"op_ack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"opId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"op_9012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"clientSeq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;391&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appliedVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1845&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"committed"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;strong&gt;committed means durably recorded under our chosen durability policy&lt;/strong&gt;, not merely received by a gateway. A separate &lt;em&gt;received&lt;/em&gt; acknowledgement could be sent earlier, but it must not be displayed as &lt;em&gt;saved&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The central challenge: concurrent edits
&lt;/h2&gt;

&lt;p&gt;Imagine the document is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alice inserts &lt;code&gt;beautiful&lt;/code&gt; immediately before &lt;code&gt;world&lt;/code&gt;. Bob, editing from the same base version, inserts &lt;code&gt;wide&lt;/code&gt; at that same position. Applying both original positional inserts without coordination can yield different results depending on arrival order.&lt;/p&gt;

&lt;p&gt;A correct collaborative protocol needs deterministic conflict rules. For simultaneous inserts at the same position, a tie-break rule (for example, an agreed ordering of operation identifiers) can produce one consistent outcome. That outcome may be &lt;code&gt;Hello beautiful wide world&lt;/code&gt; or &lt;code&gt;Hello wide beautiful world&lt;/code&gt;; &lt;strong&gt;the algorithm must make replicas agree&lt;/strong&gt;, not guess the writers' intended sentence.&lt;/p&gt;

&lt;p&gt;Naive last-write-wins whole-document saves are unsuitable because one complete document can overwrite the other. Naive application of stale character offsets is also unsafe because earlier edits shift later positions.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Operational Transform (OT): reconcile edits against newer history
&lt;/h2&gt;

&lt;p&gt;In a centralized OT-style architecture, the document owner serializes accepted operations and maintains an authoritative version sequence.&lt;/p&gt;

&lt;p&gt;Suppose Alice and Bob both start at version 42:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Alice submits an insertion based on version 42.&lt;/li&gt;
&lt;li&gt;The owner validates it, commits it as version 43, and distributes the accepted operation.&lt;/li&gt;
&lt;li&gt;Bob submits an operation also based on version 42.&lt;/li&gt;
&lt;li&gt;The owner transforms Bob's operation against the change committed at version 43, using the editor's defined transformation rules.&lt;/li&gt;
&lt;li&gt;The transformed operation is committed as version 44 and distributed.&lt;/li&gt;
&lt;li&gt;Clients reconcile their optimistic local edits with the authoritative sequence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a simple positional example, an insertion earlier in the text may shift another insertion's position. For overlapping deletes, replacements, and structured content, the rules are substantially more involved. &lt;strong&gt;OT is not simply “increment the index whenever someone inserts text.”&lt;/strong&gt; Correct implementations must handle operation types, ordering, tie-breaking, outstanding local edits, and transformation properties together.&lt;/p&gt;

&lt;p&gt;The server needs enough accepted history since &lt;code&gt;baseVersion&lt;/code&gt; to transform a stale operation. If that history has been compacted away, it cannot pretend the transform is still possible. It needs a supported snapshot-based rebase or a conflict-resolution path that may require user assistance.&lt;/p&gt;

&lt;p&gt;OT is a sensible teaching choice when one authoritative owner assigns a per-document order. It is not the only viable approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. OT vs CRDT: which should we choose?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Conflict-free Replicated Data Type (CRDT)&lt;/strong&gt; uses data structures and merge rules designed so replicas can converge despite concurrent updates. For collaborative text, CRDT approaches often associate content with stable element identifiers rather than relying solely on changing character offsets.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consideration&lt;/th&gt;
&lt;th&gt;Operational Transform&lt;/th&gt;
&lt;th&gt;CRDT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core technique&lt;/td&gt;
&lt;td&gt;Transform operations against concurrent accepted history&lt;/td&gt;
&lt;td&gt;Merge operations/state under defined CRDT rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural fit&lt;/td&gt;
&lt;td&gt;Centralized, versioned collaboration&lt;/td&gt;
&lt;td&gt;Replica-local and offline-heavy editing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main complexity&lt;/td&gt;
&lt;td&gt;Transformation correctness, outstanding edits, retained history&lt;/td&gt;
&lt;td&gt;Identifiers, causal context, metadata growth and safe garbage collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline support&lt;/td&gt;
&lt;td&gt;Possible, but requires explicit reconciliation/rebase&lt;/td&gt;
&lt;td&gt;Often a strong fit, subject to implementation details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend required?&lt;/td&gt;
&lt;td&gt;Yes for this architecture&lt;/td&gt;
&lt;td&gt;Still needed for permissions, persistence, sync, and other platform services&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither strategy automatically guarantees perfect semantic intent preservation. Neither removes the need for security, persistence, or reconnect logic. Nor does the word &lt;em&gt;CRDT&lt;/em&gt; mean every arbitrary message order and metadata-compaction scheme is safe; the particular algorithm's assumptions matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our design choice:&lt;/strong&gt; use an authoritative, OT-style per-document owner for the main architecture. A CRDT-based editor is a valid alternative, especially if offline-first behavior is a primary product requirement. This is an architectural choice, &lt;strong&gt;not a claim about Google's internal synchronization algorithm&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. One authoritative owner per document—not one server for the whole platform
&lt;/h2&gt;

&lt;p&gt;We distribute documents across a fleet of document servers. A shard router resolves &lt;code&gt;docId&lt;/code&gt; to the current owner. Each owner serializes authoritative edits for its documents and assigns monotonically increasing versions &lt;strong&gt;within each document&lt;/strong&gt;. Unrelated documents do not need a shared global operation order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document A ──► Owner 1 ──► versions A: 1, 2, 3...
Document B ──► Owner 2 ──► versions B: 1, 2, 3...
Hot Doc C  ──► Dedicated owner capacity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes concurrent-edit reasoning local to one document while allowing the overall fleet to scale horizontally. The trade-off is that one very hot document can still overwhelm its owner, so we must distinguish &lt;strong&gt;many writers&lt;/strong&gt; from &lt;strong&gt;many passive viewers&lt;/strong&gt;. Those are different bottlenecks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Owner failover and fencing
&lt;/h3&gt;

&lt;p&gt;A server being designated as the owner is not sufficient to prevent split brain. During a network partition, an old owner might still be alive when a new owner is promoted.&lt;/p&gt;

&lt;p&gt;We assign a monotonically increasing &lt;strong&gt;fencing epoch&lt;/strong&gt; to each ownership term. The durable write path validates the epoch on writes and rejects stale owners. Promotion must use a coordination mechanism that prevents conflicting epochs from both being accepted as current.&lt;/p&gt;

&lt;p&gt;If ownership is uncertain, briefly pause authoritative writes for the affected document rather than creating two valid-looking, divergent histories. Clients may continue typing into local pending queues while waiting to reconnect.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. When is an edit actually saved?
&lt;/h2&gt;

&lt;p&gt;This is one of the most important correctness boundaries in the design.&lt;/p&gt;

&lt;p&gt;Consider a dangerous implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Owner receives edit → applies in RAM → replies "saved" → crashes
                                          ↓
                              durable write never happened
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user saw &lt;em&gt;saved&lt;/em&gt;, but the edit disappeared. To avoid that failure, define a strict contract:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate authorization, operation schema, version context, and duplicate identity.&lt;/li&gt;
&lt;li&gt;Reconcile the operation with the current authoritative document state.&lt;/li&gt;
&lt;li&gt;Assign the next document version and append the accepted operation to the &lt;strong&gt;durable log&lt;/strong&gt;, using a write path that enforces the current fencing epoch.&lt;/li&gt;
&lt;li&gt;Wait for the configured durability policy to be satisfied.&lt;/li&gt;
&lt;li&gt;Only then acknowledge the edit as &lt;code&gt;committed&lt;/code&gt; and publish it as an authoritative update.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A client may render the edit locally before step 4; that is optimistic UI, not durable acceptance. If the owner crashes after the durable append but before sending the acknowledgement, the client retries the &lt;strong&gt;same &lt;code&gt;opId&lt;/code&gt;&lt;/strong&gt;. Deduplication returns the original committed result rather than applying the edit twice.&lt;/p&gt;

&lt;p&gt;The exact durability guarantee depends on storage and replication policy. A local disk append, a quorum-replicated commit, and synchronous cross-region replication provide different failure guarantees. Do not promise zero data loss during regional disasters unless the storage design actually supports it.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Operation log, snapshots, and version history
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F403zpni8657mslh7dty6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F403zpni8657mslh7dty6.png" alt="version history" width="799" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Persisting a full document body after every tiny edit is expensive. Replaying its entire lifetime of operations every time someone opens it is also expensive.&lt;/p&gt;

&lt;p&gt;Use a hybrid approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Snapshot at version 1,000
      + committed operations 1,001 ... 1,080
      = current document at version 1,080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A snapshot contains a materialized document state at an &lt;strong&gt;exact authoritative version&lt;/strong&gt;, with format/version metadata and integrity checks. The operation log records accepted changes. A bootstrap or recovering owner loads a verified snapshot and replays the verified log tail.&lt;/p&gt;

&lt;p&gt;The materialized document view and the operation history may live in different stores and &lt;strong&gt;need not be updated in one distributed transaction&lt;/strong&gt;. In this design, the durable operation log is authoritative; snapshot/materialized views are versioned projections that can be rebuilt idempotently. This is why recovery needs explicit version boundaries and reconciliation.&lt;/p&gt;

&lt;p&gt;For user-visible version history, keep snapshots or revisions according to the product's retention requirements. Restoring an earlier revision should be a &lt;strong&gt;new authorized mutation at a new current version&lt;/strong&gt;, not a silent rewind that invalidates connected clients' understanding of the timeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why compaction is subtle
&lt;/h3&gt;

&lt;p&gt;An old operation may still be needed for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transforming a client's stale OT operation.&lt;/li&gt;
&lt;li&gt;Replaying an offline client's missing history.&lt;/li&gt;
&lt;li&gt;Reconstructing a user-visible revision.&lt;/li&gt;
&lt;li&gt;Meeting audit or retention requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A snapshot does not automatically make all earlier operations disposable. Retention and compaction must be designed around these consumers, and long-offline clients need a defined fallback when the necessary history is no longer available.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Presence and cursors are not document truth
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F83n44tqfst568m0apfvm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F83n44tqfst568m0apfvm.png" alt="presence and cursors" width="800" height="676"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A collaborator's cursor position, selection, and online indicator should not have the same durability requirements as a text edit.&lt;/p&gt;

&lt;p&gt;A separate presence service tracks active &lt;strong&gt;sessions&lt;/strong&gt;, not merely user IDs. One person may have the same document open in multiple tabs; closing one tab should not make the other disappear from the presence list. Heartbeats and TTLs help expire abandoned sessions.&lt;/p&gt;

&lt;p&gt;Cursor updates can be sampled, coalesced, or dropped when clients are slow. A failed presence service may temporarily hide collaborator cursors, while committed document editing continues. Conversely, an accepted document edit must not be silently discarded just because a gateway's outbound queue is full.&lt;/p&gt;

&lt;p&gt;Cursor positions also need reconciliation with document changes. An offset meaningful at version 100 may no longer identify the same location at version 105. A practical editor transforms selection anchors or uses position representations compatible with its document model.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Offline editing and reconnection
&lt;/h2&gt;

&lt;p&gt;A disconnected user should usually be able to continue typing. The client applies changes locally and stores pending operations with stable IDs and sequencing information.&lt;/p&gt;

&lt;p&gt;On reconnect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticate and &lt;strong&gt;recheck current document permissions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Report the last known committed version and outstanding operation IDs.&lt;/li&gt;
&lt;li&gt;Determine which submitted operations were already committed but lost their acknowledgements.&lt;/li&gt;
&lt;li&gt;Fetch missing authoritative operations, if the relevant history is retained.&lt;/li&gt;
&lt;li&gt;Reconcile local pending operations against the updated state, respecting client-side ordering and the chosen OT/CRDT protocol.&lt;/li&gt;
&lt;li&gt;Resubmit unresolved operations idempotently and resume live subscriptions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the client is too far behind for incremental OT transformation, use a snapshot-based rebase &lt;strong&gt;only if the implementation has a valid way to map the local edits onto that snapshot&lt;/strong&gt;. A snapshot by itself cannot reconstruct the user's original intent. For difficult conflicts, preserve the unsynced local copy and ask for manual resolution rather than silently dropping text.&lt;/p&gt;

&lt;p&gt;If access was revoked while the user was offline, queued edits must &lt;strong&gt;not&lt;/strong&gt; be accepted on reconnect. The UI may preserve local unsynced work where appropriate, but the backend enforces the current permission state.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Authorization must work during a live session
&lt;/h2&gt;

&lt;p&gt;Checking permission only when a WebSocket opens is insufficient. An editor may be removed from a document while the socket remains connected.&lt;/p&gt;

&lt;p&gt;Enforce authorization at the authoritative mutation path and ensure that permission changes invalidate or update live subscriptions promptly. A revoked viewer must stop receiving protected document updates; a revoked editor must stop submitting accepted edits. Reconnect and history/restore endpoints need their own authorization checks too.&lt;/p&gt;

&lt;p&gt;Also validate every incoming operation's schema, size, ranges, base-version plausibility, and allowed document mutation type. A buggy or malicious client must not be able to corrupt shared state by submitting invalid operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Capacity planning: connections, operations, and fan-out
&lt;/h2&gt;

&lt;p&gt;A collaborative editor is not sized by document storage alone. Here is an &lt;strong&gt;illustrative&lt;/strong&gt;, not measured, workload:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assumption&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent actively edited documents&lt;/td&gt;
&lt;td&gt;270,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average live editors per active document&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active editing connections&lt;/td&gt;
&lt;td&gt;810,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average logical operations per active editor per second&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incoming edit operations per second&lt;/td&gt;
&lt;td&gt;648,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Additional peer deliveries at two peers per operation&lt;/td&gt;
&lt;td&gt;~1.30 million/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total open WebSocket connections including viewers&lt;/td&gt;
&lt;td&gt;3 million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heartbeat interval&lt;/td&gt;
&lt;td&gt;20 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incoming heartbeat messages per second&lt;/td&gt;
&lt;td&gt;150,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The arithmetic is useful because it separates &lt;strong&gt;ingress edit throughput&lt;/strong&gt;, &lt;strong&gt;outbound fan-out&lt;/strong&gt;, and &lt;strong&gt;connection-management load&lt;/strong&gt;. It is not a claim about Google Docs traffic.&lt;/p&gt;

&lt;p&gt;A document with three editors is different from a document with three editors and 100,000 viewers. The latter may have little write traffic but enormous broadcast pressure.&lt;/p&gt;

&lt;p&gt;To scale, distribute gateways horizontally, shard ownership by document, isolate hot documents, use gateway-local subscription fan-out, and keep per-connection buffers bounded. For large documents, consider incremental loading and efficient client rendering before attempting complex block-level authoritative sharding. Splitting a single document into independently writable blocks complicates cross-block edits, selections, and global operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Failure scenarios that decide whether the architecture is production-ready
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Correct behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket gateway crashes&lt;/td&gt;
&lt;td&gt;Reconnect to another gateway; retry unacknowledged ops using stable IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner crashes after durable commit but before ack&lt;/td&gt;
&lt;td&gt;New owner recovers committed log; retry deduplicates the op&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner crashes before durable commit&lt;/td&gt;
&lt;td&gt;Do not claim the edit was saved; client keeps it pending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two owners believe they are active&lt;/td&gt;
&lt;td&gt;Durable writer rejects stale fencing epoch; affected clients resync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broadcast misses version 105 but delivers 106&lt;/td&gt;
&lt;td&gt;Detect version gap and fetch missing history or snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot disagrees with log&lt;/td&gt;
&lt;td&gt;Reject suspect snapshot; recover from last verified checkpoint and log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presence service fails&lt;/td&gt;
&lt;td&gt;Degrade cursors/online indicators without corrupting edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission revoked mid-session&lt;/td&gt;
&lt;td&gt;Stop writes and protected subscriptions promptly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client reconnects months later&lt;/td&gt;
&lt;td&gt;Detect missing transform history; use supported rebase/manual conflict path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow subscriber accumulates updates&lt;/td&gt;
&lt;td&gt;Bound queue; drop ephemeral presence first; trigger authoritative resync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnection storm after outage&lt;/td&gt;
&lt;td&gt;Backoff with jitter, admission control, prioritized recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync algorithm deployment introduces a bug&lt;/td&gt;
&lt;td&gt;Stop affected writes if necessary, roll back, validate history and repair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region fails&lt;/td&gt;
&lt;td&gt;Recover from replicated history with higher fenced epoch; honor actual RPO/RTO&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A particularly subtle case is &lt;strong&gt;broadcast loss&lt;/strong&gt;. Pub/sub is a delivery mechanism, not the authoritative history. Every committed remote operation should carry a document version so a client can detect gaps and recover from the authoritative log. Even if the transport is generally reliable, application-level version checks remain necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. Trade-offs worth explaining in an interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OT or CRDT?&lt;/strong&gt; OT integrates naturally with a centrally ordered per-document log; CRDT can suit offline-first replication. Both require careful implementation and testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single owner or multi-writer?&lt;/strong&gt; A fenced owner simplifies ordering and debugging. Multi-writer designs may offer different availability/latency properties but introduce harder coordination and merge semantics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimistic typing or server-confirmed rendering?&lt;/strong&gt; Apply locally immediately, but distinguish pending from committed. Waiting for server round trips before rendering every keystroke harms usability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshots or operations?&lt;/strong&gt; Combine versioned snapshots with a durable operation log. Compaction must respect replay, restore, and offline requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One home region or active writers everywhere?&lt;/strong&gt; A home-region owner keeps ordering manageable, at the cost of higher network round-trip time for distant collaborators. Multi-region active writing is a separate, more complex consistency design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always accept writes or pause during uncertain failover?&lt;/strong&gt; Prefer a brief per-document write pause over divergent authoritative histories. Local pending edits can preserve the typing experience during that pause.&lt;/p&gt;

&lt;h2&gt;
  
  
  17. How to answer “Design Google Docs” in a system design interview
&lt;/h2&gt;

&lt;p&gt;A clear interview narrative is more valuable than starting with a giant infrastructure diagram:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clarify scope:&lt;/strong&gt; plain text or rich text? Typical collaborators? Offline support? History? Permissions?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State invariants:&lt;/strong&gt; eventual convergence, no falsely acknowledged edits, one authoritative version sequence per document in this design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the naive design:&lt;/strong&gt; one buffer, autosave, and why whole-document writes lose concurrent changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introduce WebSockets:&lt;/strong&gt; persistent transport, gateways, routing, and live fan-out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain concurrency:&lt;/strong&gt; an actual conflicting-edit example, followed by OT or CRDT and its limitations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make durability explicit:&lt;/strong&gt; operation IDs, commit acknowledgements, durable history, snapshots, and replay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add failure handling:&lt;/strong&gt; owner fencing, reconnect, authorization changes, missing broadcasts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale the system:&lt;/strong&gt; shard by document, separate presence, isolate hot documents, and manage large fan-out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The high-signal follow-up is often not “What is OT?” but &lt;strong&gt;“What happens when the owner crashes after sending a saved acknowledgement?”&lt;/strong&gt; Your answer should point to the durability boundary, not merely say that the client reconnects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How does a Google Docs-style editor prevent users from overwriting each other's changes?
&lt;/h3&gt;

&lt;p&gt;It sends structured edits rather than blindly replacing the entire document. An OT or CRDT synchronization protocol reconciles concurrent operations so clients can converge on a shared state. The protocol still needs correct implementation, ordering or merge semantics, and durable storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Google Docs use Operational Transform or CRDT?
&lt;/h3&gt;

&lt;p&gt;This article does &lt;strong&gt;not&lt;/strong&gt; assert which algorithm Google's current internal implementation uses. It explains two established approaches for building a Google Docs-style editor and chooses a centralized OT-style architecture as a teaching example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why are WebSockets not enough for collaborative editing?
&lt;/h3&gt;

&lt;p&gt;WebSockets move messages bidirectionally, but do not solve concurrent edits, authoritative ordering, duplicate submissions, persistence, permission revocation, or recovery from missed updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  How are edits recovered after a disconnect?
&lt;/h3&gt;

&lt;p&gt;Clients keep stable operation IDs and pending local edits. On reconnect they reauthenticate, fetch missing authoritative changes or a supported snapshot/rebase path, reconcile outstanding work, and retry idempotently. Severe conflicts may require user intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you scale a document with thousands of viewers?
&lt;/h3&gt;

&lt;p&gt;Keep one correctly fenced authoritative edit stream in the baseline design, but distribute already-committed updates through scalable fan-out infrastructure. Coalesce presence and isolate hot documents; do not confuse passive-viewer traffic with write-serialization load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final architecture: what really matters
&lt;/h2&gt;

&lt;p&gt;A reliable collaborative editor combines &lt;strong&gt;instant local editing&lt;/strong&gt; with &lt;strong&gt;authoritative, durable synchronization&lt;/strong&gt;. The core path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → WebSocket Gateway → Shard Router → Document Owner
       → Validate / Deduplicate / Reconcile
       → Durable Operation Log → Commit Ack
       → Broadcast → Other Clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Around it sit fenced ownership, versioned snapshots, offline replay, authorization, presence, and observability. The difficult engineering is not choosing a WebSocket library. It is making sure users never receive a false &lt;em&gt;saved&lt;/em&gt; signal, stale owners cannot create conflicting histories, and disconnected replicas have a safe path back to a converged state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explore the full visual system design on SeeItFlow
&lt;/h3&gt;

&lt;p&gt;This article is a condensed architectural walkthrough. For the progressive diagrams, detailed trade-offs, production failure walkthroughs, and interview questions, see the complete six-part module:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/collab-editor/complete-design" rel="noopener noreferrer"&gt;Complete Design — Google Docs / Real-Time Collaborative Editor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/collab-editor/visual-design-journey" rel="noopener noreferrer"&gt;Visual Design Journey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/collab-editor/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/collab-editor/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/collab-editor/failure-scale-scenarios" rel="noopener noreferrer"&gt;Failure &amp;amp; Scale Scenarios&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/collab-editor/interview-perspective" rel="noopener noreferrer"&gt;Interview Perspective&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;SeeItFlow is a free visual learning platform for software engineering and system design.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>distributed</category>
      <category>backend</category>
    </item>
    <item>
      <title>Recommendation System Design: Candidate Generation, Ranking &amp; Feature Stores</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:54:51 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/recommendation-system-design-candidate-generation-ranking-feature-stores-2b8</link>
      <guid>https://dev.to/mangeshmandlik/recommendation-system-design-candidate-generation-ranking-feature-stores-2b8</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the SeeItFlow series: System Design — Beyond the Architecture Diagram.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F065ng1zigqmi1dv2hc56.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F065ng1zigqmi1dv2hc56.png" alt="recommendation system final architecture" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How does a recommendation system select 20 relevant videos, products, or posts from a catalog containing millions of items—without scoring the entire catalog on every request?&lt;/p&gt;

&lt;p&gt;The answer is not simply “use machine learning.” A production recommendation system is a &lt;strong&gt;low-latency retrieval and ranking platform&lt;/strong&gt; supported by event ingestion, user profiles, feature pipelines, caches, experimentation, and safeguards against stale or ineligible results.&lt;/p&gt;

&lt;p&gt;This article develops that architecture from a simple trending feed to a personalized, production-ready design. The examples and capacities are &lt;strong&gt;illustrative engineering assumptions&lt;/strong&gt;, not claims about the internal systems of YouTube, Netflix, Amazon, or any other company.&lt;/p&gt;

&lt;p&gt;For interactive diagrams and the full six-part walkthrough, see &lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/complete-design" rel="noopener noreferrer"&gt;Recommendation System Design on SeeItFlow&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements: What are we actually building?
&lt;/h2&gt;

&lt;p&gt;Suppose we are designing recommendations for a video or commerce platform. Given a user, a recommendation surface (home feed, related items, next-watch), and request context, return a bounded, ordered list of relevant, eligible items.&lt;/p&gt;

&lt;p&gt;The system should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Personalize results when meaningful behavioral history exists.&lt;/li&gt;
&lt;li&gt;Work for anonymous users, new users, and new catalog items.&lt;/li&gt;
&lt;li&gt;React to recent user actions without rebuilding the entire long-term profile synchronously.&lt;/li&gt;
&lt;li&gt;Avoid showing deleted, restricted, unavailable, or otherwise ineligible items.&lt;/li&gt;
&lt;li&gt;Log which items were &lt;strong&gt;returned&lt;/strong&gt; and which were actually &lt;strong&gt;exposed&lt;/strong&gt;, not just which were clicked.&lt;/li&gt;
&lt;li&gt;Degrade to safe, useful recommendations when personalization services fail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Important nonfunctional requirements include latency, availability, freshness, relevance, cost, privacy, and the ability to explain quality regressions even when HTTP error rates are zero.&lt;/p&gt;

&lt;p&gt;A recommendation system is &lt;strong&gt;surface-specific&lt;/strong&gt;. Home-feed recommendations, “similar products,” and next-video autoplay may use different signals, objectives, freshness budgets, and eligibility rules. Do not assume one universal ranking policy is appropriate for every surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Estimate the workload before choosing components
&lt;/h2&gt;

&lt;p&gt;Consider an example with &lt;strong&gt;60 million daily active users&lt;/strong&gt;, each requesting recommendations &lt;strong&gt;25 times per day&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Daily recommendation requests = 60M × 25 = 1.5B
Average QPS = 1.5B / 86,400 ≈ 17,361
Illustrative 10× peak ≈ 174,000 QPS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If each uncached request scores 400 candidates, the &lt;em&gt;theoretical&lt;/em&gt; peak demand is approximately &lt;strong&gt;69.6 million candidate scores per second&lt;/strong&gt; across the fleet. Caching, cheaper pre-ranking, and partial fallbacks can reduce actual main-model scoring volume; the multiplication is a sizing upper-bound scenario, not an assertion that all requests invoke the full ranker.&lt;/p&gt;

&lt;p&gt;Now suppose the application ingests 120 behavioral events per active user per day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Behavior events/day = 60M × 120 = 7.2B
Average events/sec ≈ 83,333
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those 120 events exclude rendered impressions, the impression stream must be sized &lt;strong&gt;separately&lt;/strong&gt;. For example, a feed returning 20 items does not mean all 20 were visible to the user. Returned items and actually rendered/exposed items must not be counted interchangeably.&lt;/p&gt;

&lt;p&gt;These numbers immediately explain the architecture: &lt;strong&gt;retrieve hundreds, not millions; batch feature reads; avoid unnecessary model execution; and ingest feedback asynchronously&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. High-level architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                       CLIENTS (web / mobile / TV)
                                 |
                       Recommendation API
                                 |
                  +--------------+--------------+
                  |                             |
             Response Cache                User / Session
                  |                         Context Lookup
                  |                             |
                  +--------------+--------------+
                                 |
                         Candidate Retrieval
                   /             |              \
             Popularity     Collaborative      Embedding/ANN
                   \             |              /
                    +------ Merge &amp;amp; Dedup -----+
                                 |
                       Early Eligibility Filter
                                 |
                       Batched Feature Hydration
                                 |
                      Pre-ranker (if needed)
                                 |
                           Main Ranker
                                 |
                    Re-ranking / Diversity Rules
                                 |
                    Final Eligibility Recheck
                                 |
                      Response + Continuation

    Events / impressions -&amp;gt; Durable log -&amp;gt; Stream + batch jobs
                                         |          |
                                  Session overlay  Profiles / features
                                         |          |
                                  Online serving &amp;lt;- Published indexes/models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram separates two kinds of work. &lt;strong&gt;Offline and streaming pipelines prepare reusable data&lt;/strong&gt;. The &lt;strong&gt;online serving path&lt;/strong&gt; combines that data with the current request and produces the final result within a deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Start simple: trending recommendations
&lt;/h2&gt;

&lt;p&gt;A first version can return regional or category-level popular items from a precomputed table. This is cheap to cache, easy to operate, and useful when the system knows nothing about a user.&lt;/p&gt;

&lt;p&gt;But a trending-only feed has clear limits: it cannot distinguish a sports fan from someone interested in databases, and it may repeatedly expose the same popular items while hiding the long tail.&lt;/p&gt;

&lt;p&gt;Treat popularity as a &lt;strong&gt;candidate source and fallback&lt;/strong&gt;, not as a substitute for personalization.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Event ingestion: personalization begins with trustworthy signals
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fim18vl7vtjff15w0d8rt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fim18vl7vtjff15w0d8rt.png" alt="event ingestion" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clients and backend services emit events such as impressions, clicks, watch duration, skips, saves, hides, and purchases. A typical event envelope contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"evt_9012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"itemId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video_81"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"watch_complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"surface"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"home_feed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rec_730"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-27T10:15:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The collector authenticates where applicable, validates the schema, applies abuse controls, and durably records accepted events before acknowledging their ingestion. Downstream consumers can then process events asynchronously.&lt;/p&gt;

&lt;h3&gt;
  
  
  At-least-once delivery and duplicate events
&lt;/h3&gt;

&lt;p&gt;Network retries, broker redelivery, and operational replay can deliver the same logical event multiple times. Assign a stable event ID and make consumers idempotent. Deduplication must cover the required replay horizon, or aggregates must be rebuildable from deduplicated durable history. A short-lived recent-ID cache alone cannot guarantee correctness across an arbitrarily old replay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not claim exactly-once business effects simply because the message broker offers exactly-once-related features.&lt;/strong&gt; The end-to-end effect still depends on consumer state updates, transactional boundaries, and external stores.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event time is not processing time
&lt;/h3&gt;

&lt;p&gt;A mobile device can report a watch event minutes late. Time-window aggregations should distinguish when the action occurred from when it arrived, with watermarks or explicit late-event policies and a defined correction strategy. There is no free global event ordering across users or devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Returned is not rendered
&lt;/h3&gt;

&lt;p&gt;A recommendation API may return 20 items, but the user might only scroll far enough to see four. Maintain distinct signals for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generated:&lt;/strong&gt; retrieved by a candidate source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returned:&lt;/strong&gt; included in the API response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendered/exposed:&lt;/strong&gt; actually shown under the product's defined visibility criteria.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumed/acted upon:&lt;/strong&gt; clicked, watched, purchased, or otherwise engaged with.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A server response cannot prove an impression occurred on screen. Client impression reports may themselves be lost or duplicated, so track reporting quality and avoid treating missing client telemetry as proof that no exposure happened.&lt;/p&gt;

&lt;p&gt;Without exposure data, “no click” cannot be interpreted reliably: the item might never have been visible. This is a core source of &lt;strong&gt;selection bias&lt;/strong&gt; in recommendation training and evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Build compact user profiles, not unbounded histories
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvavrvz7wn8m8io55qki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvavrvz7wn8m8io55qki.png" alt="user profiles" width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Raw event logs are unsuitable for synchronous scanning on every recommendation request. Instead, maintain a compact user profile containing long-term category affinities, bounded recent activity, and preference summaries.&lt;/p&gt;

&lt;p&gt;Separate it from a &lt;strong&gt;short-lived session overlay&lt;/strong&gt;. If a user suddenly watches several cooking videos, a real-time processor can update session intent for the next request without immediately rewriting every long-term preference feature.&lt;/p&gt;

&lt;p&gt;A useful design is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Durable behavior log
   |--&amp;gt; Batch / stream aggregates --&amp;gt; Long-term user profile
   |--&amp;gt; Fast signal processor -----&amp;gt; Session overlay (short TTL)

Request-time context + profile + overlay --&amp;gt; Retrieval and ranking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Session signals should have bounded influence and expiry. One accidental click should not permanently redefine a user's interests. Also distinguish ordinary relevance preferences from &lt;strong&gt;privacy, consent, policy, and eligibility state&lt;/strong&gt;: the latter cannot safely depend solely on an old profile snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Candidate generation: millions of items become hundreds
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fexcmc62g1kt4ipogyj0x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fexcmc62g1kt4ipogyj0x.png" alt="candidate generation" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The candidate stage is a &lt;strong&gt;retrieval&lt;/strong&gt; problem. Its job is to find a manageable set of plausible items quickly, not to make the final ranking decision.&lt;/p&gt;

&lt;p&gt;Typical sources include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Popularity and trending.&lt;/strong&gt; Regional or category-level items with recent engagement. Strong for cold start and fallbacks, but biased toward widely exposed items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collaborative retrieval.&lt;/strong&gt; Items associated with similar interaction patterns among users or sessions. Useful when behavioral history is rich; weaker for new users and items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content-based retrieval.&lt;/strong&gt; Items similar in topic, attributes, text, creator, or other metadata. Useful for item cold start when behavioral history is missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embedding and approximate nearest-neighbor (ANN) retrieval.&lt;/strong&gt; Encode users, sessions, or queries and items into vector representations; retrieve nearby item vectors using a versioned index. ANN trades exact-neighbor recall for faster retrieval at scale. It is one retrieval strategy, not the entire recommender.&lt;/p&gt;

&lt;p&gt;Sources can run concurrently with independent deadlines and quotas. Each source should return item IDs, source provenance, index version, and optionally a source-local score. &lt;strong&gt;Raw scores from different retrieval models are not inherently comparable.&lt;/strong&gt; Merge by deduplicating IDs, applying source quotas or calibrated fusion logic, and preserving provenance for downstream features.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Popularity source:       120 candidates
Collaborative source:    200 candidates
Embedding/ANN source:    250 candidates
Content source:          100 candidates
                         ----------------
Before deduplication:    670 entries
After merge and dedupe:  bounded candidate set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact counts are examples. They are not guaranteed to sum to the final unique count because sources overlap and filters remove items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure rule:&lt;/strong&gt; one slow retrieval source should not hold the entire feed hostage. Enforce timeouts and continue with healthy sources or an approved fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Eligibility filtering must happen before &lt;em&gt;and&lt;/em&gt; after ranking
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsaljxzmgbwdonk8ffgmg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsaljxzmgbwdonk8ffgmg.png" alt="ranking funnel" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An item can be relevant and still be forbidden to show. Examples include deleted content, age-restricted media, blocked creators, region restrictions, inventory unavailability, or items the user is not entitled to access.&lt;/p&gt;

&lt;p&gt;Perform an &lt;strong&gt;early filter&lt;/strong&gt; to avoid wasting ranking work on obviously ineligible items. Then perform a &lt;strong&gt;final eligibility check&lt;/strong&gt; before returning results, including cached recommendations.&lt;/p&gt;

&lt;p&gt;Why twice? Candidate indexes and cached lists may lag behind catalog updates. A product can go out of stock after candidate generation; a video can be removed while an older index still references it.&lt;/p&gt;

&lt;p&gt;The final check must consult a source with freshness and consistency guarantees appropriate to the policy. If that source is unavailable, &lt;strong&gt;fail closed for sensitive restrictions&lt;/strong&gt; rather than silently treating an unknown state as allowed. Where strong real-time guarantees are required, the serving system needs an authoritative or appropriately synchronized eligibility path; merely lowering a cache TTL is not sufficient.&lt;/p&gt;

&lt;p&gt;A final recheck narrows the stale-data risk but does not magically make all concurrent changes impossible. Define the required consistency boundary and ensure the action endpoint (such as purchase or playback) enforces its own authorization and availability rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Feature store: the bridge between offline learning and online serving
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qhwbb4te89ikw6cwguw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qhwbb4te89ikw6cwguw.png" alt="feature store" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ranking needs more than user IDs and item IDs. Example features include recent category affinity, item popularity in a time window, item age, user-item similarity, device context, and recent negative feedback.&lt;/p&gt;

&lt;p&gt;A feature platform usually spans three related concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Definitions and metadata:&lt;/strong&gt; names, types, transformations, ownership, freshness, and schema versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline feature history:&lt;/strong&gt; reproducible features for training, backfills, and evaluation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online feature serving:&lt;/strong&gt; low-latency access to the features needed for inference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A feature store is &lt;strong&gt;not necessarily one physical database&lt;/strong&gt;, and centralizing definitions does not automatically eliminate training-serving skew.&lt;/p&gt;

&lt;h3&gt;
  
  
  Point-in-time correctness
&lt;/h3&gt;

&lt;p&gt;Suppose a model learns from a click that occurred on Monday. Its training example must not include an item-popularity count that incorporates Tuesday's clicks. That would leak future information into training and inflate offline evaluation results.&lt;/p&gt;

&lt;p&gt;Point-in-time joins must select feature values that were &lt;strong&gt;available at the prediction time&lt;/strong&gt;, accounting for both event time and feature publication/availability time where relevant. Version and timestamp the data used for reproducibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch feature hydration
&lt;/h3&gt;

&lt;p&gt;Fetching features one candidate at a time creates excessive network fan-out. Batch or multi-get the feature bundles needed for the bounded candidate set, use well-defined defaults for missing features, and check that feature versions are compatible with the selected model.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Ranking: why a retrieval score is not the final answer
&lt;/h2&gt;

&lt;p&gt;Retrieval identifies plausible items; ranking estimates their usefulness for a particular user, surface, and moment.&lt;/p&gt;

&lt;p&gt;A basic ranking pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;400 retrieved candidates
        |
  Batch feature fetch
        |
  Optional cheap pre-ranker
        |
  Expensive main ranker
        |
  Diversity / repetition controls
        |
  Final eligibility check
        |
      Top 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At smaller scale, one ranker may be sufficient. Introduce a cheaper pre-ranker when the main model's cost or latency makes scoring all retrieved candidates impractical. The trade-off is that the pre-ranker may discard items the richer model would have liked.&lt;/p&gt;

&lt;p&gt;The ranking objective may combine engagement, satisfaction, conversion, long-term retention, or other product outcomes, subject to policy constraints. These objectives can conflict. Optimizing only short-term clicks may reward sensational or repetitive results while reducing long-term satisfaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A model score is not automatically a calibrated probability.&lt;/strong&gt; Depending on the training objective, it may be a relative ordering signal, an uncalibrated logit, or a calibrated estimate of a defined outcome. Do not compare scores across model versions as if they share a universal scale unless calibration and comparability are established.&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-ranking and diversity
&lt;/h3&gt;

&lt;p&gt;After model scoring, re-ranking can cap repeated creators, diversify categories, suppress recently consumed items, and reserve a controlled number of exploration slots. Such rules should be measured: more diversity can improve discovery but may reduce immediate predicted engagement.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Cold start: new users and new items are different problems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;New user:&lt;/strong&gt; no meaningful personal history. Use region-appropriate popularity, contextual information, optional onboarding preferences, and exploration. Personalization can improve as reliable events accumulate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New item:&lt;/strong&gt; little or no interaction history. Use metadata/content-based retrieval and carefully controlled exploration to create opportunities for exposure. An item may also receive an embedding derived from content before any interactions occur; therefore, it is incorrect to assume that every new item must wait for behavioral history or a scheduled embedding rebuild.&lt;/p&gt;

&lt;p&gt;Track new-item exposure and catalog coverage. Otherwise, the feedback loop can permanently favor already-popular items because those are the only ones receiving training data.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Real-time recommendations without recomputing everything
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzmrl9q6wdpv5xopuya0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzmrl9q6wdpv5xopuya0a.png" alt="real time recommendations" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A session overlay lets the system react quickly to recent actions. For example, after a user skips three similar videos, the next feed request can reduce that category's short-term weight.&lt;/p&gt;

&lt;p&gt;This is usually &lt;strong&gt;near-real-time&lt;/strong&gt;, not a hard guarantee that every action is reflected instantly. Processing lag, network delay, caching, and concurrent requests can all affect visibility.&lt;/p&gt;

&lt;p&gt;Use targeted cache invalidation on strong signals, short-lived overlay state, and freshness SLOs appropriate to each surface. Keep slower long-term aggregation in batch or streaming pipelines based on its actual business need—not because all features must update on every event.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Caching and pagination: fast results must still be correct
&lt;/h2&gt;

&lt;p&gt;Cache regional trending lists, hot item metadata, frequently requested features, and, where useful, short-lived personalized recommendation results.&lt;/p&gt;

&lt;p&gt;Personalized cache keys must be scoped to the right user/session, surface, locale, relevant context, policy version, and model/index version. Omitting a user-specific dimension can cause a &lt;strong&gt;cross-user cache leak&lt;/strong&gt;, which is a privacy issue, not just a relevance issue.&lt;/p&gt;

&lt;p&gt;Use request coalescing and soft TTL refresh to prevent many simultaneous misses from overwhelming retrieval and ranking services. Never let a cache hit bypass the final eligibility check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Infinite scroll needs session consistency
&lt;/h3&gt;

&lt;p&gt;Page 1 and page 2 may be generated seconds apart, during which item popularity, user activity, or model versions can change. Naively rerunning retrieval can show duplicate items or skip previously ranked ones.&lt;/p&gt;

&lt;p&gt;A continuation token can bind requests to a short-lived recommendation snapshot, candidate-set reference, or seen-item state. The token should be tamper-resistant and expire appropriately. It does not have to contain the entire candidate list; it can reference server-side state.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Model and index rollouts: silent failures are the real risk
&lt;/h2&gt;

&lt;p&gt;Recommendation failures often return HTTP 200. A new model can be fully available and still show worse content.&lt;/p&gt;

&lt;p&gt;For a model rollout, use offline validation, compatibility checks, shadow evaluation where useful, a controlled canary or A/B experiment, progressive expansion, and rollback thresholds based on both system health and recommendation quality.&lt;/p&gt;

&lt;p&gt;For an index rollout, build a new version alongside the old one, validate its integrity and coverage, then publish it through a versioned pointer or explicitly controlled gradual rollout. Retain the prior good version for rollback. An atomic pointer update in one metadata store does &lt;strong&gt;not&lt;/strong&gt; imply every serving process worldwide instantly switches at the same moment; use version pinning and rollout visibility to manage mixed versions safely.&lt;/p&gt;

&lt;p&gt;A/B assignment should be sticky and based on stable identifiers where appropriate, and the assigned variant must be propagated into exposure and outcome logs. Guard against sample-ratio mismatches, missing telemetry, and nonrandom logging loss before trusting experiment conclusions.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Failure scenarios worth designing for
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;User-visible or silent impact&lt;/th&gt;
&lt;th&gt;Safe response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ranking service overloaded&lt;/td&gt;
&lt;td&gt;Slow feed, timeouts&lt;/td&gt;
&lt;td&gt;Reduce candidates, use cheaper model, serve safe cached fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One candidate source times out&lt;/td&gt;
&lt;td&gt;Lower retrieval coverage&lt;/td&gt;
&lt;td&gt;Respect deadline; merge healthy sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature store unavailable or incompatible&lt;/td&gt;
&lt;td&gt;Generic or incorrectly scored items&lt;/td&gt;
&lt;td&gt;Use known-compatible defaults/snapshots; never bypass hard policy checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event ingestion lag&lt;/td&gt;
&lt;td&gt;Recent actions do not influence feed&lt;/td&gt;
&lt;td&gt;Track event age; use bounded session state when available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad model or index rollout&lt;/td&gt;
&lt;td&gt;Worse relevance with HTTP 200&lt;/td&gt;
&lt;td&gt;Detect via quality metrics; roll back version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deleted or restricted item in stale cache&lt;/td&gt;
&lt;td&gt;Correctness or policy breach&lt;/td&gt;
&lt;td&gt;Mandatory current-state eligibility check; fail closed for sensitive rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate events&lt;/td&gt;
&lt;td&gt;Inflated popularity/profile signals&lt;/td&gt;
&lt;td&gt;Stable IDs, idempotent updates, replay-safe reconstruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trending item creates hot key&lt;/td&gt;
&lt;td&gt;Counter and cache pressure&lt;/td&gt;
&lt;td&gt;Pre-aggregate, shard hot counters, coalesce reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One source dominates retrieval&lt;/td&gt;
&lt;td&gt;Narrow, repetitive feed&lt;/td&gt;
&lt;td&gt;Monitor source share; quotas and diversity controls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-user cache-key collision&lt;/td&gt;
&lt;td&gt;Privacy leak&lt;/td&gt;
&lt;td&gt;Strict key scoping, tests, purge and incident response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Quality monitoring must go beyond p95 latency and error rate. Track candidate counts, per-source contribution, feature freshness, fallback rate, rendered impressions, exposure concentration, new-item coverage, repeat-item rate, and experiment outcomes. A system that is fast and always returns 200 can still be failing its users.&lt;/p&gt;

&lt;p&gt;For more detailed incident walk-throughs, see &lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/failure-scale-scenarios" rel="noopener noreferrer"&gt;Recommendation System Failure &amp;amp; Scale Scenarios&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. Key architectural trade-offs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Popularity vs. personalization:&lt;/strong&gt; popularity is cheap and resilient; personalization improves relevance but needs behavioral state and more online work. Use a deliberate blend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precomputation vs. freshness:&lt;/strong&gt; compute reusable embeddings, features, and indexes offline; reserve request-time work for context-dependent decisions. Use streaming only where freshness earns its cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-source vs. multi-source retrieval:&lt;/strong&gt; one source is simple; several sources improve coverage and resilience but introduce merge complexity and extra fan-out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exact search vs. ANN:&lt;/strong&gt; exact vector search can be appropriate for small catalogs or offline evaluation; approximate retrieval becomes attractive when exact search exceeds the online budget. Benchmark recall, latency, memory, and update behavior for the chosen workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-stage vs. multi-stage ranking:&lt;/strong&gt; a pre-ranker reduces expensive model load but may eliminate valuable candidates too early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relevance vs. exploration:&lt;/strong&gt; serving only the current highest-scoring items reinforces exposure bias; exploration creates information about underexposed content, at a measurable short-term cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eventual freshness vs. hard correctness:&lt;/strong&gt; most relevance signals tolerate bounded staleness; policy, authorization, deletion, and inventory-sensitive decisions need stronger checks appropriate to their semantics.&lt;/p&gt;

&lt;p&gt;For the reasoning behind these choices, read &lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/tradeoffs-decisions" rel="noopener noreferrer"&gt;Recommendation System Trade-offs &amp;amp; Decisions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  17. How to explain this in a system design interview
&lt;/h2&gt;

&lt;p&gt;Start with the surface and the objective. A “recommendation system” is not one fixed product requirement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq43oru4rg68129tvolzd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq43oru4rg68129tvolzd.png" alt="architecture evolve" width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then evolve the architecture in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trending baseline:&lt;/strong&gt; deliver a useful list without personalization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event collection:&lt;/strong&gt; record durable actions and exposure context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profiles:&lt;/strong&gt; summarize history into fast-serving state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Candidate generation:&lt;/strong&gt; narrow the catalog using multiple bounded retrieval sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranking:&lt;/strong&gt; score only a manageable candidate set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature store:&lt;/strong&gt; make online inference and offline training consistent and reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time overlay:&lt;/strong&gt; respond to current-session intent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production safeguards:&lt;/strong&gt; enforce eligibility, caching discipline, versioned rollouts, experimentation, and degraded modes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Interviewers often ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not rank every item?&lt;/strong&gt; Because the cost of expensive per-item inference across a huge catalog exceeds the latency budget. Retrieval narrows the search space first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why log impressions rather than just clicks?&lt;/strong&gt; Because an unclicked item may never have been exposed. Without exposure data, negative labels and offline evaluation can be biased.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when the feature store is down?&lt;/strong&gt; Fall back to known-compatible features or a cheaper model where safe; independently enforce policy and eligibility using an appropriate current-state source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle new items?&lt;/strong&gt; Metadata/content retrieval, content-derived embeddings where available, and controlled exploration rather than relying solely on historical engagement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you detect a bad ranking model?&lt;/strong&gt; Monitor controlled experiments and quality metrics, not only API errors and latency; retain a fast rollback path.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/interview-perspective" rel="noopener noreferrer"&gt;Recommendation System Interview Perspective&lt;/a&gt; for a larger question bank.&lt;/p&gt;

&lt;h2&gt;
  
  
  18. Final takeaway
&lt;/h2&gt;

&lt;p&gt;The core of recommendation system design is a &lt;strong&gt;sequence of bounded decisions&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Collect reliable signals → build compact state → retrieve plausible candidates → hydrate compatible features → rank and diversify → recheck eligibility → log actual exposure → learn and improve.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Machine-learning models matter, but so do event semantics, feature freshness, latency budgets, cache isolation, model compatibility, exposure bias, and safe degradation. Neglecting any of those can produce recommendations that look technically healthy while quietly becoming irrelevant—or incorrect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explore the complete visual walkthrough on SeeItFlow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/complete-design" rel="noopener noreferrer"&gt;Complete Design — requirements, sizing, and architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/visual-design-journey" rel="noopener noreferrer"&gt;Visual Design Journey — step-by-step evolution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive — event contracts, features, ranking, and rollouts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions — engineering choices and consequences&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/failure-scale-scenarios" rel="noopener noreferrer"&gt;Failure &amp;amp; Scale Scenarios — production incident analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/recommendation-system/interview-perspective" rel="noopener noreferrer"&gt;Interview Perspective — questions and strong answer directions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;SeeItFlow is free to use, with interactive visual explanations and no sign-in required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>machinelearning</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>YouTube System Design: Upload, Transcoding, CDN &amp; Video Streaming</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Mon, 19 Oct 2026 07:04:45 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/youtube-system-design-upload-transcoding-cdn-video-streaming-2pi0</link>
      <guid>https://dev.to/mangeshmandlik/youtube-system-design-upload-transcoding-cdn-video-streaming-2pi0</guid>
      <description>&lt;h2&gt;
  
  
  YouTube System Design: From Video Upload to Global Playback
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxu5gqrkgk0zqvuuc9ncq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxu5gqrkgk0zqvuuc9ncq.png" alt="youtube complete architecture" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A viewer presses Play. A few seconds later, video is streaming smoothly, even on a changing mobile network. That looks simple until you consider what happened before the first frame: a creator uploaded a large file over an unreliable connection; the platform validated it, transcoded it into compatible formats, published a consistent set of segments, and distributed those bytes across a global caching network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Designing a YouTube-like video-sharing platform is really two different system-design problems:&lt;/strong&gt; safely turning uploads into playable assets, and delivering those assets to enormous numbers of viewers at low latency and reasonable cost.&lt;/p&gt;

&lt;p&gt;This article walks through both, including the failure cases that often get omitted from architecture diagrams. It describes a &lt;em&gt;proposed architecture&lt;/em&gt;, not YouTube's proprietary internal implementation.&lt;/p&gt;

&lt;p&gt;For an interactive explanation with diagrams and step-by-step flows, see &lt;a href="https://seeitflow.com/system-design/intermediate/youtube/complete-design" rel="noopener noreferrer"&gt;Video Streaming Platform System Design on SeeItFlow&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Scope and requirements
&lt;/h2&gt;

&lt;p&gt;We'll design &lt;strong&gt;video-on-demand (VOD)&lt;/strong&gt;: creators upload videos, and viewers discover and play them later. Live streaming, DRM internals, recommendation algorithms, ads, and moderation internals are separate systems and are outside this design.&lt;/p&gt;

&lt;p&gt;The functional requirements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload large videos, with pause/resume and per-part retries.&lt;/li&gt;
&lt;li&gt;Process originals into playable representations; generate thumbnails and metadata.&lt;/li&gt;
&lt;li&gt;Publish only validated, complete media outputs.&lt;/li&gt;
&lt;li&gt;Stream at adaptive quality, with seeking and playback recovery.&lt;/li&gt;
&lt;li&gt;Support public and restricted visibility, search, metadata edits, and deletion.&lt;/li&gt;
&lt;li&gt;Record view events and show an eventually consistent display count.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The non-functional requirements drive the architecture: durable finalized uploads, safe retries, bounded processing delays, fast startup, low rebuffering, global delivery, origin protection, and explicit privacy/deletion semantics.&lt;/p&gt;

&lt;p&gt;A useful separation is &lt;strong&gt;ingest/processing&lt;/strong&gt;, &lt;strong&gt;control/metadata&lt;/strong&gt;, and &lt;strong&gt;delivery/playback&lt;/strong&gt;. A video being uploaded, processed, discoverable, and playable are distinct states; one flag cannot safely represent all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Estimate the workload before choosing infrastructure
&lt;/h2&gt;

&lt;p&gt;In a video platform, playback demand will often dwarf upload count. Rather than inventing real-world YouTube statistics, use clearly stated assumptions.&lt;/p&gt;

&lt;p&gt;Suppose, purely as a capacity-planning example, we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100,000 uploads/day, averaging 500 MB each.&lt;/li&gt;
&lt;li&gt;1 million simultaneous viewers at peak.&lt;/li&gt;
&lt;li&gt;An average delivered bitrate of 3 Mbps per active viewer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upload ingress:&lt;/strong&gt; 100,000 × 500 MB ≈ &lt;strong&gt;50 TB/day&lt;/strong&gt; of original uploads, before replication and derived media.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peak playback bandwidth:&lt;/strong&gt; 1,000,000 × 3 Mbps ≈ &lt;strong&gt;3 Tbps&lt;/strong&gt; delivered to viewers. That is an aggregate delivery-network figure, not a claim that a single origin must serve it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Processing work:&lt;/strong&gt; depends on &lt;em&gt;media duration&lt;/em&gt;, resolution, codec, and how many representations are encoded. A two-hour 4K upload and a 20-second clip should not count as equal-sized processing jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage:&lt;/strong&gt; originals + transcoded representations + audio + thumbnails + retention/replication overhead. Segmentation mostly divides a rendition into smaller objects; &lt;strong&gt;additional renditions and codecs&lt;/strong&gt; are what significantly multiply encoded-media storage.&lt;/p&gt;

&lt;p&gt;These illustrative numbers explain why the upload service, processing workers, and playback delivery tier need to scale independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. High-level architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATOR
   |
   | 1. Create upload session
   v
Upload API / Metadata DB
   |  short-lived, scoped upload credentials
   v
Object Storage (original media) &amp;lt;---- Creator uploads parts directly
   |
   | 2. Verified, idempotent finalize
   v
Metadata transaction + durable processing job / outbox
   |
   v
Dispatcher -&amp;gt; Processing Queue -&amp;gt; Transcoding Workers
                                     |
                                     v
                              Probe / Encode / Package
                                     |
                                     v
                           Validate versioned outputs
                                     |
                                     v
                             Atomic publish pointer
                                     |
                                     v
                              Object Storage (origin)
                                     |
                                     v
VIEWER -&amp;gt; Playback API / Authorization -&amp;gt; CDN Edge -&amp;gt; Shield -&amp;gt; Origin
                 |                        |
                 v                        v
             Metadata DB              HLS/DASH segments
                 |
                 +--&amp;gt; Search indexing / async view aggregation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a logical diagram. Actual implementations may combine services or use different managed products. The important properties are the durability boundaries and which dependencies sit on the critical playback path.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Resumable uploads: don't make the app server carry every byte
&lt;/h2&gt;

&lt;p&gt;Sending a multi-gigabyte file through the application tier consumes network bandwidth and connections for work that object storage can handle more efficiently.&lt;/p&gt;

&lt;p&gt;A safer upload flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The creator authenticates and requests an upload session. The server creates a stable &lt;code&gt;upload_id&lt;/code&gt;, records ownership, size limits, and expiry.&lt;/li&gt;
&lt;li&gt;The server issues short-lived, tightly scoped upload permissions for the intended object/session.&lt;/li&gt;
&lt;li&gt;The client uploads independently identifiable parts directly to object storage, retrying failed parts without restarting the entire file.&lt;/li&gt;
&lt;li&gt;The client asks to finalize. The backend checks authoritative storage-side part/completion state and applicable integrity constraints. It does &lt;strong&gt;not&lt;/strong&gt; trust a client-side “all chunks uploaded” assertion.&lt;/li&gt;
&lt;li&gt;Once the object is finalized and metadata/job state is committed, the client receives a stable video identity and a &lt;strong&gt;processing&lt;/strong&gt; status—not a promise that playback is ready.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finalization must be &lt;strong&gt;idempotent&lt;/strong&gt;. If the server commits and its response is lost, retrying the same &lt;code&gt;upload_id&lt;/code&gt; must return the existing result rather than create a second video or an unrelated processing workflow.&lt;/p&gt;

&lt;p&gt;There is an important ordering nuance: object-storage multipart completion and a metadata database transaction are not automatically one atomic operation. The implementation needs a recoverable state machine and a reconciliation sweep for cases where storage finalization succeeds but metadata completion fails, or vice versa. A durable source object must not be silently orphaned or incorrectly treated as ready.&lt;/p&gt;

&lt;p&gt;Expired sessions and abandoned multipart uploads need automated cleanup. Upload credentials must never grant broad bucket access.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Durable processing: the database-to-queue gap
&lt;/h2&gt;

&lt;p&gt;Video processing is CPU/GPU intensive and highly variable in duration. It belongs outside the upload request.&lt;/p&gt;

&lt;p&gt;A tempting implementation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE videos SET state = 'PROCESSING';
publish_to_queue(video_id);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But these are two separate systems. The database commit can succeed while publishing to the queue fails, leaving a creator waiting forever.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;transactional outbox&lt;/strong&gt; or equivalent durable job record closes that gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One database transaction:
  - Mark the upload finalized / processing pending
  - Insert processing_job(job_id, source_version, state='PENDING')
Commit

Separate dispatcher:
  - Read undispatched jobs
  - Publish to processing queue
  - Record dispatch progress after broker acknowledgement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A crash after queue publish but before recording success may cause duplicate delivery. That's acceptable only because processing is designed to be &lt;strong&gt;idempotent at its publication boundary&lt;/strong&gt;. An outbox gives durable &lt;em&gt;at-least-once handoff&lt;/em&gt;, not exactly-once execution.&lt;/p&gt;

&lt;p&gt;Each job should identify its source version, processing profile, and attempt. Retry transient failures with backoff and limits; quarantine deterministic failures such as unsupported codecs or irreparably corrupt sources. Monitor &lt;strong&gt;oldest job age, queued media minutes, and processing SLO attainment&lt;/strong&gt;, not just raw queue depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Transcoding: a rendition is more than a resolution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfhsakdmll4mr5muw49f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfhsakdmll4mr5muw49f.png" alt="transcoding pipeline" width="799" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv8gi677jbyl6kde7v2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv8gi677jbyl6kde7v2w.png" alt="multiple rendition" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A creator might upload a high-bitrate file in a format that some viewers cannot decode or stream efficiently. The platform produces multiple compatible representations.&lt;/p&gt;

&lt;p&gt;A representation can vary in resolution, bitrate, codec, frame rate, audio format, and HDR profile. An illustrative ladder might include 1080p, 720p, 480p, and 360p, but &lt;strong&gt;there is no universal ladder&lt;/strong&gt;. Choose representations based on source characteristics, device compatibility, network diversity, encoding cost, and expected demand. Avoid upscaling merely to populate a preset rung.&lt;/p&gt;

&lt;p&gt;A practical processing workflow is a dependency graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Probe and validate source
        |
        +--&amp;gt; Video encode: representation A --+
        +--&amp;gt; Video encode: representation B --+--&amp;gt; Package/segment
        +--&amp;gt; Audio encode --------------------+         |
        +--&amp;gt; Thumbnail extraction                      v
                                                   Validate
                                                      |
                                                   Publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Independent representations can process concurrently. Track progress and failure per stage, so a failed optional rendition can be retried without recomputing everything. Whether a reduced ladder is publishable depends on an explicit &lt;strong&gt;minimum-playable-set policy&lt;/strong&gt;; it is not always all-or-nothing.&lt;/p&gt;

&lt;p&gt;Encoding is expensive. Precomputing every possible quality improves first-play readiness but wastes work on rarely viewed videos. Selective or on-demand encoding can reduce cost, at the price of more complex coordination and potentially higher first-view latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The most important correctness boundary: safe publication
&lt;/h2&gt;

&lt;p&gt;Imagine a transcoding worker crashes after writing half the segments. Its job is redelivered while the original worker is still running. If both attempts overwrite the same public object keys, a viewer may see a manifest referencing incomplete or mixed output.&lt;/p&gt;

&lt;p&gt;The solution is &lt;strong&gt;attempt-scoped staging and immutable published versions&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each processing attempt writes into a private, unique staging namespace.&lt;/li&gt;
&lt;li&gt;The system verifies required representations, segments, manifest references, and basic media integrity.&lt;/li&gt;
&lt;li&gt;All objects referenced by the candidate manifest must already be durably readable before publication.&lt;/li&gt;
&lt;li&gt;A conditional/transactional metadata update changes the video's &lt;strong&gt;active published version&lt;/strong&gt; only if the candidate is valid and the publishing worker still has authority.&lt;/li&gt;
&lt;li&gt;Older published versions remain intact for in-flight readers and rollback until a safe retention/cleanup policy removes them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The atomic operation is the &lt;strong&gt;metadata pointer change&lt;/strong&gt;, not an imaginary transaction spanning thousands of object-storage writes. Duplicate or late workers may waste compute, but they cannot publish over the active version unless they pass the required fencing/conditional checks.&lt;/p&gt;

&lt;p&gt;Versioned URLs are also CDN-friendly: a segment at &lt;code&gt;video/v7/720p/segment-42.m4s&lt;/code&gt; never changes its bytes. Reprocessing creates &lt;code&gt;v8&lt;/code&gt; instead of silently mutating &lt;code&gt;v7&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. HLS, MPEG-DASH, and adaptive bitrate streaming
&lt;/h2&gt;

&lt;p&gt;The platform packages media into short, time-aligned segments and manifests/playlists that tell the player which representations and segments exist.&lt;/p&gt;

&lt;p&gt;Two common adaptive streaming families are &lt;strong&gt;HLS&lt;/strong&gt; and &lt;strong&gt;MPEG-DASH&lt;/strong&gt;. Their exact manifest structures differ: for example, HLS commonly uses a multivariant playlist and media playlists, whereas DASH uses an MPD describing adaptation sets/representations and segment addressing. They should not be described as byte-for-byte identical formats.&lt;/p&gt;

&lt;p&gt;For a typical segmented VOD session:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The player gets authorized playback information and a manifest URL.&lt;/li&gt;
&lt;li&gt;It selects a supported representation using device capability, estimated throughput, and current buffer health.&lt;/li&gt;
&lt;li&gt;It requests media segments over HTTP and buffers ahead.&lt;/li&gt;
&lt;li&gt;When throughput drops, it requests subsequent segments at a lower sustainable bitrate; when conditions improve, it may step up cautiously.&lt;/li&gt;
&lt;li&gt;On seeking, it locates the segment covering the requested playback time rather than downloading everything from the beginning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rendition switching works best when segments and suitable random-access/keyframe boundaries are aligned. A startup algorithm &lt;strong&gt;does not always begin at the lowest resolution&lt;/strong&gt;. Nor should it blindly chase the highest bitrate: rebuffering usually hurts the viewing experience more than a temporary reduction in picture quality.&lt;/p&gt;

&lt;p&gt;Segment duration is a trade-off: shorter segments allow finer-grained adaptation but increase request and packaging overhead. It is a design choice, not a fixed number that every streaming service must use.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. CDN design: hot videos are a caching problem—with exceptions
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcoxcs8egkfogzvlc7ta7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcoxcs8egkfogzvlc7ta7.png" alt="CDN" width="799" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A million viewers cannot all fetch every segment from one storage origin without enormous egress and request pressure. A CDN places popular segments closer to viewers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Viewer -&amp;gt; CDN edge (hit: serve locally)
                |
                +-- miss -&amp;gt; Regional/shield cache
                                  |
                                  +-- miss -&amp;gt; Object-storage origin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immutable segments can use long-lived, versioned cache keys. Manifests and authorization-related responses may require different caching policies.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;origin shield&lt;/strong&gt; reduces the number of separate edge misses that reach origin. Request coalescing helps prevent multiple simultaneous misses for the same object from triggering duplicate upstream fetches. Neither makes origin load literally zero: cold content, cache-key fragmentation, regional traffic skew, and purge events still matter.&lt;/p&gt;

&lt;p&gt;One subtle bug can destroy the cache hit ratio: including a viewer-specific signed token in the cache key when the underlying segment bytes are identical. Normalize the cache key &lt;em&gt;only if authorization is still independently enforced before cached bytes are served&lt;/em&gt;. Removing tokens from the key without enforcing access at the CDN or trusted delivery layer would create a security vulnerability.&lt;/p&gt;

&lt;p&gt;A broad CDN outage also needs a bounded fallback strategy. Sending unlimited global traffic directly to origin can turn a CDN incident into an origin outage. Consider alternate healthy edges or providers where configured, controlled origin fallback, and load shedding.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Metadata, visibility, and playback authorization
&lt;/h2&gt;

&lt;p&gt;A video record might track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;video_id, creator_id, title, description, duration,
source_object, processing_state, active_media_version,
visibility, moderation_state, created_at, updated_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;strong&gt;processing readiness&lt;/strong&gt;, &lt;strong&gt;discoverability&lt;/strong&gt;, and &lt;strong&gt;authorization&lt;/strong&gt; separate. A video can have a valid encoded output but still be private, blocked, or intentionally unpublished.&lt;/p&gt;

&lt;p&gt;For restricted content, the playback API verifies entitlement and issues a short-lived credential that the delivery path actually checks. A metadata flag alone does not protect segments if anyone with a cached URL can fetch them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unlisted is not the same as private.&lt;/strong&gt; In a common product model, unlisted means omitted from normal discovery but accessible to people with the link; private means access is restricted to authorized viewers. The exact policy should be stated, not assumed.&lt;/p&gt;

&lt;p&gt;A public-to-private change requires more than updating one database row:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stop issuing new access credentials and make the authoritative authorization decision reflect the restriction.&lt;/li&gt;
&lt;li&gt;Invalidate or bypass stale authorization caches, including remote-region replicas where necessary.&lt;/li&gt;
&lt;li&gt;Remove the video from search/discovery asynchronously, with priority over ordinary metadata edits.&lt;/li&gt;
&lt;li&gt;Bound or revoke previously issued delivery credentials and purge cached media when the policy requires it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Short-lived signed URLs do not provide instantaneous revocation by themselves.&lt;/strong&gt; Already-issued credentials may remain valid until expiry unless the delivery architecture supports a revocation check or equivalent enforcement. CDN purge propagation is also not necessarily instantaneous. A correct design states the permitted exposure window and chooses enforcement accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Search and view counts are separate workloads
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96ox7629sv06bmx85gcl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96ox7629sv06bmx85gcl.png" alt="view count" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Metadata lives in a transactional store; video binaries live in object storage. As catalog size and ranking needs grow, a dedicated search index supports text matching and ranking without forcing search traffic through the metadata database.&lt;/p&gt;

&lt;p&gt;Index updates are usually asynchronous. Search may briefly show stale results, but &lt;strong&gt;playback authorization remains the final access gate&lt;/strong&gt;. A search outage should not break playback for a viewer who already has a valid video link.&lt;/p&gt;

&lt;p&gt;View counting has its own scale trap. Incrementing one database row synchronously for every view creates a hot write key for viral content. Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Player event -&amp;gt; Event ingestion -&amp;gt; Stream/queue
                                  |
                                  v
                         Dedupe + aggregation
                                  |
                                  v
                       Periodic counter updates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First define what counts as a view; there is no universal watch-time threshold. Events need stable identities for bounded deduplication, and a viral video may require sharded or hierarchical aggregation to avoid a single hot reducer.&lt;/p&gt;

&lt;p&gt;The user-visible count can be &lt;strong&gt;approximate and eventually consistent&lt;/strong&gt; if the product permits it. Bounded dedupe does not guarantee an exact lifetime count, and a dropped event may never be recovered. Financially consequential metrics—such as ad billing or creator payouts—require a separate, stricter measurement and audit design.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Multi-region delivery is not the same as multi-region ownership
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzs1vndpmunl3fhoqikm3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzs1vndpmunl3fhoqikm3.png" alt="multi-region" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A CDN can serve cached segments worldwide, but it does not automatically make upload processing, metadata writes, or source objects resilient to a regional failure.&lt;/p&gt;

&lt;p&gt;At a large enough scale, regional ingest and origin infrastructure can improve locality and availability. That adds harder questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which region owns writes for a given video or metadata partition?&lt;/li&gt;
&lt;li&gt;Where are originals and derived objects replicated?&lt;/li&gt;
&lt;li&gt;Can a region serving a cache miss actually reach a copy of that segment?&lt;/li&gt;
&lt;li&gt;How do we prevent two regions from accepting conflicting authoritative writes during a partition?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One workable model uses a single authoritative write owner per video/partition, asynchronous read replicas, selective object replication, and &lt;strong&gt;fencing&lt;/strong&gt; before write ownership moves. An in-progress multipart upload tied to a failed region may have to restart; seamless continuation requires explicit design, not a routing rule.&lt;/p&gt;

&lt;p&gt;Selective replication saves storage and transfer cost but means some content may become unavailable when its only reachable origin fails. The availability promise must match the actual replication policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Failure scenarios that test whether the design is real
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What can go wrong&lt;/th&gt;
&lt;th&gt;Safe response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Upload finalize response lost&lt;/td&gt;
&lt;td&gt;Client retries and creates duplicate work&lt;/td&gt;
&lt;td&gt;Stable &lt;code&gt;upload_id&lt;/code&gt;, idempotent finalize, recoverable state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata commit succeeds, queue publish fails&lt;/td&gt;
&lt;td&gt;Video remains processing forever&lt;/td&gt;
&lt;td&gt;Durable outbox/job record and retrying dispatcher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker lease expires while worker still runs&lt;/td&gt;
&lt;td&gt;Two attempts race to publish&lt;/td&gt;
&lt;td&gt;Attempt-specific staging and fenced, conditional publication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One rendition fails&lt;/td&gt;
&lt;td&gt;Partial ladder exists&lt;/td&gt;
&lt;td&gt;Retry failed stage or apply an explicit minimum-rendition policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest published before segments&lt;/td&gt;
&lt;td&gt;Early viewers get 404s&lt;/td&gt;
&lt;td&gt;Validate object availability before switching active version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Viral video's first few requests&lt;/td&gt;
&lt;td&gt;Cold-edge misses overwhelm origin&lt;/td&gt;
&lt;td&gt;Coalescing, origin shield, rate limiting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN cache-key fragmentation&lt;/td&gt;
&lt;td&gt;Cache hit ratio collapses&lt;/td&gt;
&lt;td&gt;Normalize safe keys; enforce authorization independently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network bandwidth collapses&lt;/td&gt;
&lt;td&gt;Buffer drains, playback stalls&lt;/td&gt;
&lt;td&gt;ABR downshift, buffer-aware retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy change meets stale cache&lt;/td&gt;
&lt;td&gt;Restricted video remains accessible&lt;/td&gt;
&lt;td&gt;Authoritative access checks, revocation strategy, cache invalidation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region fails&lt;/td&gt;
&lt;td&gt;Some origins or metadata owners disappear&lt;/td&gt;
&lt;td&gt;Replication-aware routing and fenced failover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telemetry pipeline fails&lt;/td&gt;
&lt;td&gt;View count stops updating&lt;/td&gt;
&lt;td&gt;Keep playback independent; buffer or accept bounded event loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video deletion stalls&lt;/td&gt;
&lt;td&gt;Artifacts remain at CDN/storage&lt;/td&gt;
&lt;td&gt;Durable, observable deletion workflow with retries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few details are worth emphasizing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A failed transcode should not destroy an already-playable video.&lt;/strong&gt; Reprocessing writes a new version and leaves the current version active until validation succeeds. Rollback should restore a known-good version pointer, while respecting current privacy/deletion state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A missing segment is not always a transient network error.&lt;/strong&gt; Repeated 404s may signal a publication race, incorrect manifest, or missing origin replica. Player retries can help temporarily, but they do not replace fixing the source inconsistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A deletion is a workflow, not one SQL statement.&lt;/strong&gt; Stop new authorization promptly, update discovery, revoke delivery as policy requires, and track eventual cleanup of CDN and stored artifacts. Do not promise immediate worldwide physical deletion without an enforcement mechanism that actually provides it.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. What to measure in production
&lt;/h2&gt;

&lt;p&gt;A successful architecture needs observability for both creator and viewer experiences.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;uploads&lt;/strong&gt;, track session success, part retries, finalize latency, abandoned sessions, orphaned objects, and finalization reconciliation failures.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;processing&lt;/strong&gt;, track oldest-job age, queued media minutes, stage/rendition failure rate, retry/DLQ growth, processing SLO attainment, and cost per encoded media minute.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;playback&lt;/strong&gt;, track startup time to first frame, rebuffer ratio, fatal playback errors, bitrate/quality switches, segment 4xx/5xx, CDN hit ratio, shield misses, and origin egress.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;security and lifecycle&lt;/strong&gt;, monitor authorization denials, privacy/deletion propagation lag, revoked-credential behavior, stuck deletion jobs, and unexpected access to deleted/private media.&lt;/p&gt;

&lt;p&gt;Keep operational dashboards separated by region, video/version, codec, and device class where practical; otherwise a small but severe compatibility or regional problem may disappear inside global averages.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Architectural trade-offs: no universal answer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Baseline choice&lt;/th&gt;
&lt;th&gt;When to reconsider&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Upload bytes&lt;/td&gt;
&lt;td&gt;Direct-to-object-storage multipart upload&lt;/td&gt;
&lt;td&gt;Small prototype or inline inspection requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing&lt;/td&gt;
&lt;td&gt;Asynchronous jobs with durable handoff&lt;/td&gt;
&lt;td&gt;Tiny files and truly negligible processing work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encoding&lt;/td&gt;
&lt;td&gt;Precompute a useful baseline ladder&lt;/td&gt;
&lt;td&gt;Large long tail makes selective/lazy encoding worthwhile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media publication&lt;/td&gt;
&lt;td&gt;Immutable versions + atomic active pointer&lt;/td&gt;
&lt;td&gt;Generally retain; this is a correctness mechanism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery&lt;/td&gt;
&lt;td&gt;CDN + shield + protected origin&lt;/td&gt;
&lt;td&gt;Single-region, tiny viewer population&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;Dedicated index at sufficient scale&lt;/td&gt;
&lt;td&gt;Database full-text search may suffice early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display views&lt;/td&gt;
&lt;td&gt;Async, approximate aggregation&lt;/td&gt;
&lt;td&gt;Audited financial counters need stronger guarantees&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regions&lt;/td&gt;
&lt;td&gt;Start simple; add regional origins when justified&lt;/td&gt;
&lt;td&gt;Strict global availability/upload locality requires them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN providers&lt;/td&gt;
&lt;td&gt;Single provider initially&lt;/td&gt;
&lt;td&gt;Provider-level availability or routing requirements justify multi-CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point of a system-design interview is not to name every possible component. It is to explain &lt;strong&gt;which requirement forces each component to exist&lt;/strong&gt;, and how the design behaves when that component fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. How to present this in a system-design interview
&lt;/h2&gt;

&lt;p&gt;Start by clarifying VOD versus live, upload versus playback scope, privacy, target scale, and SLOs. Estimate ingest, processing work, storage, and peak playback egress. Then draw the ingest/processing and delivery paths separately.&lt;/p&gt;

&lt;p&gt;Spend your deepest technical time on four boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload finalization:&lt;/strong&gt; what does the server actually know, and how do retries behave?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable job handoff:&lt;/strong&gt; what happens if the database commits but queue publication fails?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe media publication:&lt;/strong&gt; can duplicate workers or incomplete segments ever become viewer-visible?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playback and privacy:&lt;/strong&gt; how do CDN caching, ABR, and access revocation coexist?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then test the design with a viral-video spike, a processing backlog, a CDN outage, and a public-to-private transition. These scenarios reveal much more than adding more boxes to the architecture diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A robust video platform doesn't become reliable just because it has object storage, Kafka, transcoding workers, and a CDN. It becomes reliable when the transitions between those systems are explicit: an upload is verified before being finalized, a processing job cannot be silently lost, incomplete media cannot be published, cached bytes cannot bypass authorization, and retries cannot corrupt a previously good version.&lt;/p&gt;

&lt;p&gt;That is the difference between a diagram that looks scalable and a video-streaming architecture that can survive production failures.&lt;/p&gt;




&lt;h3&gt;
  
  
  Explore the full video streaming system design on SeeItFlow
&lt;/h3&gt;

&lt;p&gt;The article is a condensed, standalone walkthrough. For the full visual and technical treatment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/youtube/complete-design" rel="noopener noreferrer"&gt;Complete Design — requirements, architecture, and end-to-end flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/youtube/visual-design-journey" rel="noopener noreferrer"&gt;Visual Design Journey — follow the architecture as it evolves&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/youtube/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive — processing, media packaging, CDN, and lifecycle mechanics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/youtube/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions — alternatives and why each choice fits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/youtube/failure-scale-scenarios" rel="noopener noreferrer"&gt;Failure &amp;amp; Scale Scenarios — stress-test the design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/youtube/interview-perspective" rel="noopener noreferrer"&gt;Interview Perspective — questions, estimates, and follow-ups&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SeeItFlow&lt;/strong&gt; is a free visual learning platform for software engineering and system design: &lt;a href="https://seeitflow.com/" rel="noopener noreferrer"&gt;seeitflow.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>backend</category>
      <category>scalability</category>
    </item>
    <item>
      <title>Ticket Booking System Design: Prevent Double Booking at Scale</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:53:46 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/ticket-booking-system-design-prevent-double-booking-at-scale-434m</link>
      <guid>https://dev.to/mangeshmandlik/ticket-booking-system-design-prevent-double-booking-at-scale-434m</guid>
      <description>&lt;h2&gt;
  
  
  Ticket Booking System Design: How to Prevent Double Booking Under Extreme Demand
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft22ck2pp6sas70u9elxo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft22ck2pp6sas70u9elxo.png" alt="ticket booking final architecture" width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At 10:00 a.m., tickets for a popular concert go on sale. Thousands of customers refresh the same seat map. Two people click the same front-row seat almost simultaneously. Both see it as available. Both press &lt;strong&gt;Reserve&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Only one of them can own it.&lt;/p&gt;

&lt;p&gt;That sounds like a small database problem, but a production ticket-booking system must preserve that guarantee while handling abandoned checkouts, delayed payments, expired holds, retries, lost messages, and sudden traffic spikes. Preventing the first double-booking race is relatively straightforward. Preventing an old expiry task from releasing a &lt;em&gt;new&lt;/em&gt; customer's seat—or a late payment from confirming a seat already sold to someone else—is where the design gets interesting.&lt;/p&gt;

&lt;p&gt;This article develops a practical architecture for assigned-seat booking, such as concert, cinema, or stadium ticketing. General-admission inventory is a related variant, but its capacity-counter model differs from the per-seat model used here.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with the invariants, not the microservices
&lt;/h2&gt;

&lt;p&gt;The primary invariant is that &lt;strong&gt;one event-specific seat can have at most one live owner&lt;/strong&gt;: either an active, valid hold or a confirmed booking. A seat must never be confirmed for two unrelated bookings.&lt;/p&gt;

&lt;p&gt;A second invariant is that &lt;strong&gt;one logical customer request should not create multiple holds or bookings merely because the client retries&lt;/strong&gt;. Atomicity and idempotency solve different problems. Atomicity stops competing buyers from acquiring the same seat. Idempotency stops a repeated request from being interpreted as a new purchase attempt.&lt;/p&gt;

&lt;p&gt;Other requirements follow from those two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can browse events, inspect seat maps, select one or more seats, pay, receive tickets, view their bookings, and cancel when policy permits.&lt;/li&gt;
&lt;li&gt;A temporary hold must expire safely if checkout is abandoned.&lt;/li&gt;
&lt;li&gt;Payment outcomes may be &lt;strong&gt;successful, failed, or unknown&lt;/strong&gt;; a timeout does not prove failure.&lt;/li&gt;
&lt;li&gt;Availability displays may be slightly stale, but reservation decisions must use authoritative state.&lt;/li&gt;
&lt;li&gt;Under exceptional demand, the system should shed or pace excess work without weakening reservation correctness.&lt;/li&gt;
&lt;li&gt;Booking, payment, and ticket-issuance discrepancies must be discoverable and recoverable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We should also clarify whether the event uses &lt;strong&gt;assigned seats&lt;/strong&gt; or &lt;strong&gt;general admission&lt;/strong&gt;. For assigned seats, a customer purchases a particular event-specific seat, such as Section A, Row 3, Seat 12. For general admission, a customer purchases a quantity from a bounded capacity pool. Both must prevent overselling, but they need different inventory operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A useful first-cut architecture
&lt;/h2&gt;

&lt;p&gt;A booking platform benefits from two deliberately different paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browsing path:&lt;/strong&gt; Client → API gateway → Availability service → cache or read model. This path returns seat maps quickly. The underlying view can be a cached snapshot, a read replica, or a materialized projection. Its freshness target is a product decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reservation path:&lt;/strong&gt; Client → admission control, where needed → Booking service → authoritative transactional database. This path creates durable holds using atomic operations. It never grants a reservation because the cache says a seat is available.&lt;/p&gt;

&lt;p&gt;The Booking service coordinates with a separate payment integration. After a valid confirmation, an outbox relay publishes a &lt;code&gt;booking.confirmed&lt;/code&gt; event. Ticket issuance and notifications consume that event asynchronously and idempotently. Expiry workers release abandoned holds, while reconciliation workers investigate unresolved or inconsistent states.&lt;/p&gt;

&lt;p&gt;Not every deployment needs a separate service for each box. A smaller system can implement several responsibilities in one application while retaining the same transactional boundaries and invariants. The important separation is between &lt;strong&gt;displaying availability&lt;/strong&gt; and &lt;strong&gt;authoritatively allocating inventory&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Model the event, seat, hold, booking, and payment separately
&lt;/h2&gt;

&lt;p&gt;A physical seat belongs to a venue, but the sellable inventory unit belongs to a particular event. Seat A12 can be sold for Friday's concert and independently for Saturday's concert. Therefore, the unique sellable identity is event-scoped, conceptually &lt;code&gt;(event_id, seat_id)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A minimal domain model looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Event&lt;/td&gt;
&lt;td&gt;Schedule, sale window, venue, inventory mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event seat&lt;/td&gt;
&lt;td&gt;Event-specific sellable seat, pricing zone, authoritative allocation state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hold&lt;/td&gt;
&lt;td&gt;Durable identity, owner, selected seats, creation/expiry time, lifecycle state and version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Booking&lt;/td&gt;
&lt;td&gt;Customer purchase intent and confirmation lifecycle, linked to hold and payment reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment&lt;/td&gt;
&lt;td&gt;Provider-owned outcome and attempt/reference information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket&lt;/td&gt;
&lt;td&gt;Usable admission entitlement issued for a confirmed booking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A hold is &lt;strong&gt;not&lt;/strong&gt; just &lt;code&gt;seat.status = HELD&lt;/code&gt;. It has an identity (&lt;code&gt;hold_id&lt;/code&gt;) that survives retries and lets delayed workers prove which allocation they are acting on. A multi-seat hold also gives several seats one shared logical reservation identity.&lt;/p&gt;

&lt;p&gt;A possible set of separate state machines is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwm9oy5zz0adm7508g1f7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwm9oy5zz0adm7508g1f7.png" alt="state machine" width="799" height="324"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hold:     ACTIVE → CONFIRMED
                 ↘ EXPIRED / RELEASED

Booking:  CREATED → PAYMENT_PENDING → CONFIRMED
                  ↘ FAILED / CANCELLED (as applicable)

Payment:  PENDING / UNKNOWN → SUCCEEDED / FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are illustrative state names, not a requirement to use precisely these enums. Cancellation, refund, and ticket validity can each have additional independent states. A booking can be payment-pending while its hold is active; it can also be confirmed while a refund is pending. Combining everything into one &lt;code&gt;booked&lt;/code&gt; boolean loses essential information.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why a simple availability check double-books seats
&lt;/h2&gt;

&lt;p&gt;Consider a naive implementation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxig7zdeq0ridvzt7mb3i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxig7zdeq0ridvzt7mb3i.png" alt="race condition" width="800" height="330"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A reads A12 → AVAILABLE
User B reads A12 → AVAILABLE
User A writes A12 → HELD
User B writes A12 → HELD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both requests made a decision from an earlier observation. The sequence &lt;strong&gt;read → decide → write&lt;/strong&gt; is not atomic. Application-level checks do not fix that race when concurrent requests can interleave.&lt;/p&gt;

&lt;p&gt;For a single assigned seat, a conditional update in an authoritative transactional database is a strong baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;event_seats&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'HELD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hold_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;hold_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;held_until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AVAILABLE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1hw1j5qy3aca8q4hz76.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1hw1j5qy3aca8q4hz76.png" alt="atomic reservation" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Exactly one competing update can transition a currently available row to held, assuming the database provides the expected atomic update semantics and all allocation writers use the same authoritative ownership rules. The winner sees one affected row; the loser sees zero and gets a seat-conflict response.&lt;/p&gt;

&lt;p&gt;The seat update, creation of the durable hold, and the request's idempotency record should commit in &lt;strong&gt;one transaction&lt;/strong&gt;. A database uniqueness constraint on &lt;code&gt;(event_id, seat_id)&lt;/code&gt; identifies the sellable row; an allocation constraint or guarded transition must also prevent competing live owners if allocations are stored in a separate table. Otherwise a crash could leave an allocated seat without the durable identity needed to manage it safely.&lt;/p&gt;

&lt;p&gt;An alternative is optimistic concurrency using &lt;code&gt;WHERE version = :expected_version&lt;/code&gt;; another is &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; with a short row lock. All can enforce the invariant. The choice depends on contention and the number of related rows being changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never keep a database row lock open while the customer completes payment.&lt;/strong&gt; The lock protects the short transaction that establishes the durable hold. The hold's persisted ownership and expiry—not a long-running transaction—protect the checkout window.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Reserving multiple seats must be all-or-none
&lt;/h2&gt;

&lt;p&gt;Suppose a customer selects A1, A2, and A3. If A2 is already held, silently reserving A1 and A3 usually violates the purchase contract.&lt;/p&gt;

&lt;p&gt;When all selected seats share one transactional database boundary, acquire or conditionally update them in a &lt;strong&gt;single transaction&lt;/strong&gt;. Process seats in a deterministic order, such as ascending seat ID, verify that every requested seat was acquired, and commit only if the complete set succeeded. If any seat conflicts, roll back the entire attempt.&lt;/p&gt;

&lt;p&gt;Deterministic acquisition order reduces deadlocks: otherwise one request could lock A1 then wait for A2 while another locks A2 then waits for A1. It does not eliminate every possible database deadlock, so transactions still need sensible error handling and bounded retries for genuinely transient failures.&lt;/p&gt;

&lt;p&gt;Cross-partition bookings are harder. If seats are distributed across separate transactional shards, there is no automatic all-or-none commit. Options include constraining multi-seat selections to one partition or implementing coordinated temporary sub-holds with compensation and reconciliation. A saga can aim for an &lt;strong&gt;eventual all-or-none business outcome&lt;/strong&gt; using provisional sub-holds and compensation, but it does not magically provide a single instantaneous atomic transaction across shards. During partial progress, provisional allocations must remain unavailable to competing buyers; compensation can fail and therefore requires durable retries and reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Hold idempotency protects against lost responses
&lt;/h2&gt;

&lt;p&gt;Imagine the booking database commits a hold, but the HTTP response is lost. The customer presses Reserve again. Without idempotency, the server may treat the retry as a second independent request.&lt;/p&gt;

&lt;p&gt;A stable idempotency key should identify the logical hold request. Persist the key and a request fingerprint—such as event ID and sorted selected seat IDs—under a durable uniqueness constraint, alongside the hold creation transaction. On retry:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the key already exists with the same fingerprint, return the existing hold and its current state, including its original expiry.&lt;/li&gt;
&lt;li&gt;If the same key is reused for a different seat selection, reject the conflict.&lt;/li&gt;
&lt;li&gt;If the key is new, attempt the atomic reservation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Concurrent requests using the same key must also be serialized or resolved through the database's uniqueness enforcement. Merely checking for an existing key before inserting one is another check-then-act race.&lt;/p&gt;

&lt;p&gt;A retry must not silently extend the original hold's expiry or create a fresh hold unless the product explicitly defines and safely implements that separate operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Expiry is another concurrency problem
&lt;/h2&gt;

&lt;p&gt;Temporary holds prevent other buyers from taking a seat while the first buyer pays. But abandoned holds cannot occupy inventory forever. A server-side &lt;code&gt;expires_at&lt;/code&gt; establishes the business validity window; the countdown shown in the browser is only a display.&lt;/p&gt;

&lt;p&gt;The subtle bug is a &lt;strong&gt;delayed expiry worker releasing the wrong customer's seat&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00  Hold H1 owns A12
10:05  H1 expires and A12 is released
10:06  Hold H2 acquires A12
10:07  A delayed expiry task for H1 runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the task executes &lt;code&gt;release A12 WHERE state = 'HELD'&lt;/code&gt;, it releases H2, even though H2 is legitimate.&lt;/p&gt;

&lt;p&gt;The release must identify the &lt;em&gt;specific old hold&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;event_seats&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AVAILABLE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hold_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;held_until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;hold_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;expired_hold_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'HELD'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;held_until&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db_now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;:db_now&lt;/code&gt; represents a consistently evaluated authoritative time, ideally derived from the database's time semantics. The seat release and transition of the corresponding hold to its terminal state must be coordinated transactionally when they share the same database. In particular, a worker must not mark the hold expired and commit before releasing its seats in an unrelated transaction: that can strand inventory or create inconsistent ownership. For a multi-seat hold, release the complete set according to its lifecycle contract, not as unrelated best-effort seat updates.&lt;/p&gt;

&lt;p&gt;A delayed or duplicated expiry task now becomes a safe no-op if the seat has changed owners. Confirmation must use equally strict ownership, state, and expiry checks. Expiry and confirmation cannot independently succeed for the same hold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worker lag is not extra hold validity.&lt;/strong&gt; Even if an expiry worker is down, reservation and confirmation operations must honor the persisted expiry semantics. A hold with &lt;code&gt;expires_at &amp;lt;= authoritative_now&lt;/code&gt; is no longer eligible for ordinary confirmation, even if its stored state has not yet been cleaned up. A new reservation may need to reclaim an expired allocation transactionally rather than wait for a background scan, but it must do so using the same identity/version protections.&lt;/p&gt;

&lt;p&gt;Expiry workers can use indexed &lt;code&gt;(state, expires_at)&lt;/code&gt; scans, bounded batches, delayed messages, or durable workflow timers. A Redis sorted set can accelerate scheduling but should not become the only authority for seat ownership. Every mechanism needs idempotent, identity-scoped release. Monitor the oldest overdue hold and expiry-processing lag.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Payment success and seat ownership are separate facts
&lt;/h2&gt;

&lt;p&gt;A successful payment is necessary for a paid booking, but &lt;strong&gt;payment success alone cannot grant a seat&lt;/strong&gt;. The Booking service must still prove that the corresponding hold validly owns the requested inventory when confirmation commits.&lt;/p&gt;

&lt;p&gt;The payment service can return three broad outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Known success:&lt;/strong&gt; Attempt confirmation through the guarded authoritative transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known failure:&lt;/strong&gt; Transition the booking appropriately and release the hold safely, provided no other payment attempt or outcome remains unresolved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown:&lt;/strong&gt; Preserve the ambiguity and resolve it through supported provider status queries, authenticated webhooks, safe same-identity retries where explicitly supported, or reconciliation reports and escalation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A timeout is not evidence of failure. Retrying a charge as a brand-new provider operation can create a duplicate payment. A PSP status-query endpoint is useful &lt;strong&gt;when available&lt;/strong&gt;, but it is not required by the design: authenticated callbacks, settlement/reconciliation files, and provider escalation can be the supported resolution channels. If none gives definitive evidence by an operational deadline, move the case to an unresolved exception queue—not automatically to &lt;code&gt;FAILED&lt;/code&gt;. Payment and booking each need durable, stable identities so their histories can be correlated.&lt;/p&gt;

&lt;p&gt;The hardest case occurs when a payment succeeds &lt;strong&gt;after the hold expires&lt;/strong&gt;. There are two materially different outcomes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The seat is still unallocated.&lt;/strong&gt; The product may choose to attempt an atomic reacquisition and confirm, but only if that reacquisition wins against all competing buyers. Alternatively, it can void or refund the payment. It cannot pretend the expired hold was still valid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The seat has been reallocated.&lt;/strong&gt; The original customer must not reclaim it. The booking becomes an exception requiring void/refund or another explicitly supported recovery policy, customer communication, and reconciliation. The newer customer's legitimate allocation remains untouched.&lt;/p&gt;

&lt;p&gt;Extending a hold for a bounded period after payment initiation can reduce late-success incidents. It cannot guarantee their elimination: a provider outage or delayed reconciliation can outlast any finite timeout.&lt;/p&gt;

&lt;p&gt;The critical implementation detail is that booking confirmation, hold transition, seat ownership finalization, and the durable event-to-publish should commit together when they share one authoritative transactional database. If the design splits these facts across stores, it needs explicit coordination, compensation, and reconciliation rather than an assumed atomic commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Booking confirmation is not the same as ticket issuance
&lt;/h2&gt;

&lt;p&gt;Once payment and ownership checks succeed, the booking is committed as confirmed. The system must then issue one or more tickets and notify the customer. These downstream operations can fail independently.&lt;/p&gt;

&lt;p&gt;A direct sequence—commit booking, then publish &lt;code&gt;booking.confirmed&lt;/code&gt;—has a dual-write gap. The process may crash after committing but before publishing. The customer has a confirmed booking but never receives a ticket.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;transactional outbox&lt;/strong&gt; closes this durability gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN TRANSACTION
  Verify valid hold ownership and payment-success evidence
  Mark booking CONFIRMED
  Finalize hold and seat allocation
  Insert booking.confirmed into outbox
COMMIT

Outbox relay → broker → ticket issuance / notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relay can crash after publishing but before marking the outbox row delivered. It may therefore publish again. &lt;strong&gt;Outbox delivery is not automatically exactly-once.&lt;/strong&gt; Consumers must be idempotent.&lt;/p&gt;

&lt;p&gt;For ticket issuance, a stable business uniqueness constraint such as &lt;code&gt;(booking_id, seat_id)&lt;/code&gt; for assigned seating—or &lt;code&gt;(booking_id, ticket_sequence)&lt;/code&gt; for quantity-based tickets—prevents a repeated event from minting a second independent entitlement. The constraint must reflect the actual product's ticket identity and reissue rules.&lt;/p&gt;

&lt;p&gt;If ticket generation fails, the already-confirmed paid booking should remain confirmed. Retry issuance, alert on missing entitlements, and provide a way for the customer to retrieve the ticket later. Notification failures should similarly not undo a valid purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Cached seat maps can be wrong without corrupting bookings
&lt;/h2&gt;

&lt;p&gt;Seat-map reads are frequent and often vastly outnumber successful reservations. Serving every refresh from the authoritative write database would create unnecessary contention during popular on-sales.&lt;/p&gt;

&lt;p&gt;A cache or read model can be slightly stale in either direction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale positive:&lt;/strong&gt; The UI shows A12 as available, but someone else already holds it. The authoritative reservation rejects the attempt. This hurts conversion, not correctness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale negative:&lt;/strong&gt; The UI still shows A12 as held after its hold expired and released it. Another customer misses an opportunity until the view refreshes. Again, a display issue rather than a double-allocation.&lt;/p&gt;

&lt;p&gt;Choose a freshness target based on product expectations. TTLs, event-driven invalidation, read-model updates, polling, and WebSocket/SSE pushes are implementation choices, not correctness mechanisms. Push updates make the interface feel live; they do not make a reservation authoritative.&lt;/p&gt;

&lt;p&gt;If the cache fails, do not let every client request fall through to the booking database at once. Rate-limit, serve acceptable stale snapshots, reduce refresh frequency, or shed nonessential browsing traffic to protect the reservation path.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. A virtual waiting room protects the bottleneck; it does not allocate seats
&lt;/h2&gt;

&lt;p&gt;A blockbuster event might attract far more simultaneous buyers than the booking database or payment provider can safely handle. Scaling API servers alone does not increase the authoritative database's ability to commit conflicting seat transitions.&lt;/p&gt;

&lt;p&gt;A virtual waiting room controls &lt;strong&gt;admission to the booking flow&lt;/strong&gt;. It can issue scoped, time-limited admission tokens and pace entry at a measured rate based on the slowest critical dependency: booking-write throughput, payment processing capacity, and operational ability to expire and reconcile holds.&lt;/p&gt;

&lt;p&gt;The waiting room is a fairness and load-management mechanism, not a reservation guarantee. A user admitted to checkout can still lose a seat to another user who wins the atomic allocation first. Queue position may be approximate; reconnects and token reuse need deliberate handling. A token must not grant extra admission or bypass booking idempotency.&lt;/p&gt;

&lt;p&gt;A backend message queue is not the same thing as a user-visible waiting room. An internal queue buffers work between services; a waiting room deliberately gates users before they enter a scarce-inventory checkout experience. For smaller events, clear rejection and backoff may be simpler than operating a waiting room.&lt;/p&gt;

&lt;p&gt;The system should distinguish &lt;strong&gt;temporarily unavailable&lt;/strong&gt; (all seats held, some may return) from &lt;strong&gt;fully sold&lt;/strong&gt; (all inventory confirmed or otherwise permanently unavailable). Otherwise it may unnecessarily stop admission while abandoned holds are about to expire.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Sharding by event ID does not solve one hot event
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb02qbdlyb6z0e6bf6ejg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb02qbdlyb6z0e6bf6ejg.png" alt="high demand event" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Partitioning by &lt;code&gt;event_id&lt;/code&gt; is useful because unrelated concerts or screenings no longer compete on the same database partition. But a single major concert still concentrates all its reservation traffic on one shard.&lt;/p&gt;

&lt;p&gt;For that case, finer partitioning by event → section/block → seat can distribute independent seat operations. The trade-off is important: a multi-seat request crossing partitions no longer benefits from one local all-or-none transaction.&lt;/p&gt;

&lt;p&gt;There is no universal rule that every system should shard down to individual seats. Choose partition boundaries using observed demand, transaction patterns, and the product's multi-seat requirements. Admission control may be sufficient before the complexity of cross-partition reservation is justified.&lt;/p&gt;

&lt;p&gt;Multi-region ownership adds another layer of risk. Two regions must not independently grant the same seat during a network partition or failover. A single authoritative writer per seat/partition, with explicit leader fencing and safe failover, is a practical default. A naive active-active setup is not a harmless availability optimization; it can violate the core inventory invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. An end-to-end reservation and confirmation contract
&lt;/h2&gt;

&lt;p&gt;It helps to connect the components into one precise workflow. The following is a &lt;strong&gt;logical sequence&lt;/strong&gt;, not a claim that every deployment needs these exact APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /events/{eventId}/holds
  Idempotency-Key: &amp;lt;stable request key&amp;gt;
  body: { seatIds: ["A1", "A2"] }
  → 201 { holdId, expiresAt, state: "ACTIVE" }
  → 409 if any seat cannot be acquired

POST /bookings/{bookingId}/payment
  → starts or retrieves the payment operation with its stable identity
  → { paymentState: "PENDING" | "UNKNOWN" | "SUCCEEDED" | "FAILED" }

GET /bookings/{bookingId}
  → returns the durable booking, hold, and ticket status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server must authenticate and authorize each request; a caller cannot select another user's hold simply by knowing its identifier. The seat IDs must belong to the requested event and sale window, prices and eligibility must be validated on the server, and any client-submitted price must not be trusted. The booking must bind the charged amount and currency to the server-approved order snapshot; if price or seat selection changes, the system must create an explicitly new authorized operation rather than silently reusing an old payment identity.&lt;/p&gt;

&lt;p&gt;The hold creation transaction must make three things agree: the seats' allocation, the durable hold row, and the idempotency mapping. A second transaction later confirms the booking &lt;strong&gt;only when payment-success evidence and current, unexpired ownership are both valid&lt;/strong&gt;. In the local-transaction baseline, that confirmation also finalizes seats and inserts the outbox event. No database transaction stays open across the external payment call.&lt;/p&gt;

&lt;p&gt;For an asynchronous payment method, a &lt;code&gt;PENDING&lt;/code&gt; response is a legitimate checkout result, not a failure. A client can poll its own booking-status API; the backend may learn the financial outcome through whichever provider channels actually exist. An API reporting &lt;em&gt;our recorded booking state&lt;/em&gt; must not be confused with an API querying &lt;em&gt;the provider's payment state&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Ticket validity and cancellation need their own rules
&lt;/h2&gt;

&lt;p&gt;Issuing a ticket is not equivalent to authorizing entry. A scannable ticket should map to a durable entitlement with a unique identity and a defined validity lifecycle. A duplicate &lt;code&gt;booking.confirmed&lt;/code&gt; delivery must retrieve or reuse that entitlement, not mint a second independently usable one. For a venue that checks tickets at the gate, the scan path must also enforce the product's admission rule—typically preventing a single-use entitlement from being accepted twice. Offline scanners and multi-gate operation require their own synchronization/conflict policy; the booking database alone cannot guarantee online, globally coordinated admission when scanners are disconnected.&lt;/p&gt;

&lt;p&gt;Cancellation is another state transition, not a synonym for refund completion. Validate the cancellation window and whether the ticket has already been used; revoke or invalidate the entitlement where required, release inventory only when resale is permitted, and initiate a separate refund workflow. Refunds can be pending or ambiguous just like charges. A used ticket or an offline gate that has not received a revocation requires an explicit policy rather than a claim of instant global invalidation.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Failures that should be in the design review
&lt;/h2&gt;

&lt;p&gt;A useful review asks what the system does &lt;strong&gt;after&lt;/strong&gt; a partial success, not just whether the happy path works.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Required safe behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hold committed, response lost&lt;/td&gt;
&lt;td&gt;Retry with the same idempotency key returns the existing hold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two users select one seat&lt;/td&gt;
&lt;td&gt;Only one authoritative transition wins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One seat conflicts in a multi-seat request&lt;/td&gt;
&lt;td&gt;Roll back all seats if the contract is all-or-none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old expiry task runs after a new hold&lt;/td&gt;
&lt;td&gt;Identity-scoped release affects zero rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expiry worker stops&lt;/td&gt;
&lt;td&gt;Expiry remains a business rule; backlog drains in bounded batches after recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment times out&lt;/td&gt;
&lt;td&gt;Remain unresolved; never infer a decline or create a fresh independent charge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment succeeds after seat reallocation&lt;/td&gt;
&lt;td&gt;Preserve the new owner's seat; resolve the original payment via exception handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate or out-of-order webhook&lt;/td&gt;
&lt;td&gt;Deduplicate and validate allowed state transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Booking commit succeeds, event publish fails&lt;/td&gt;
&lt;td&gt;Durable outbox row is retried by relay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket consumer runs twice&lt;/td&gt;
&lt;td&gt;Business-key uniqueness prevents duplicate usable entitlements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability cache fails&lt;/td&gt;
&lt;td&gt;Degrade browsing without overwhelming the booking-write database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One hot event saturates a partition&lt;/td&gt;
&lt;td&gt;Apply event-specific admission control; consider finer partitioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region fails during reservation&lt;/td&gt;
&lt;td&gt;Fence the former writer; reconcile uncertain in-flight operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two additional operational cases deserve explicit policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cancellation versus ticket use:&lt;/strong&gt; A cancelled booking does not mean its refund has succeeded. If tickets are scanned for admission, cancellation must coordinate ticket revocation with the admission-validation system. A ticket already used before cancellation may require a policy exception or manual review; a simple database status update is insufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hold storms:&lt;/strong&gt; A large number of users may acquire seats but abandon payment, making an event look sold out even though many seats will return. Track hold-to-confirmation conversion and active-hold pressure, not just raw reservation attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. Reconciliation is part of the architecture, not an incident script
&lt;/h2&gt;

&lt;p&gt;Retries and asynchronous messaging do not resolve every ambiguity. Reconciliation continuously compares the booking system's durable records with payment evidence, inventory ownership, outbox delivery, and ticket issuance.&lt;/p&gt;

&lt;p&gt;Examples include paid-but-pending bookings, successful payments attached to expired holds, confirmed bookings without the expected payment evidence, seats marked held without a matching active hold, confirmed bookings without tickets, unpublished outbox events, and cancellations with unresolved refunds.&lt;/p&gt;

&lt;p&gt;Reconciliation jobs should be &lt;strong&gt;idempotent, bounded, observable, and safe to rerun&lt;/strong&gt;. A provider status API, when offered, can help operational reconciliation; otherwise use the provider's supported reports, settlement evidence, callbacks, or escalation. Settlement may arrive substantially later than the checkout request, so unresolved financial cases need an age-based escalation policy without inventing a success/failure outcome. They should apply the same guarded state transitions as live requests, not bypass correctness checks in the name of repair. Financially sensitive or ambiguous cases should enter an exception workflow rather than be guessed into a terminal state.&lt;/p&gt;

&lt;p&gt;Metrics that matter include reservation conflict rate per event/seat, active holds and expiry lag, payment-unknown age, booking-confirmation latency, reconciliation mismatch count and oldest unresolved case, outbox lag, ticket-issuance lag, waiting-room depth, and booking-database saturation.&lt;/p&gt;

&lt;h2&gt;
  
  
  17. The architectural trade-offs worth explaining
&lt;/h2&gt;

&lt;p&gt;A strong design does not claim every mechanism is always necessary. It explains which risk justifies each mechanism.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Sensible baseline&lt;/th&gt;
&lt;th&gt;What changes the choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seat concurrency&lt;/td&gt;
&lt;td&gt;Conditional atomic update&lt;/td&gt;
&lt;td&gt;Complex multi-row transactions may favor short row locks; versions help with other concurrent updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout&lt;/td&gt;
&lt;td&gt;Durable temporary hold&lt;/td&gt;
&lt;td&gt;Very fast purchase flows or ample general admission may use different sequencing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-seat booking&lt;/td&gt;
&lt;td&gt;One local transaction&lt;/td&gt;
&lt;td&gt;Cross-shard selections need constraints or compensation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expiry&lt;/td&gt;
&lt;td&gt;Indexed scan with safe conditional release&lt;/td&gt;
&lt;td&gt;Delay queues/timers can improve scheduling; none replace ownership checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Cache/read model with freshness target&lt;/td&gt;
&lt;td&gt;High-demand UX may justify invalidation or push updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic spikes&lt;/td&gt;
&lt;td&gt;Admission control sized to write-path capacity&lt;/td&gt;
&lt;td&gt;Ordinary demand may need only rate limits/rejections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event partitioning&lt;/td&gt;
&lt;td&gt;Event-level isolation&lt;/td&gt;
&lt;td&gt;One hot event may need section-level partitioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirmation events&lt;/td&gt;
&lt;td&gt;Transactional outbox&lt;/td&gt;
&lt;td&gt;Simpler direct publishing leaves a crash-induced dual-write gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regions&lt;/td&gt;
&lt;td&gt;One fenced write owner per partition&lt;/td&gt;
&lt;td&gt;Active-active requires explicit conflict-prevention architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key is to distinguish optimizations from guarantees. A faster seat map improves UX; it does not prevent double booking. A longer hold improves checkout conversion; it does not guarantee payment resolution. A waiting room improves admission control; it does not guarantee inventory. An outbox improves durable event delivery; it does not eliminate duplicate processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  18. How to explain this in a system design interview
&lt;/h2&gt;

&lt;p&gt;Start with the two workloads: &lt;strong&gt;fast, potentially stale availability reads&lt;/strong&gt; and &lt;strong&gt;correct, authoritative reservation writes&lt;/strong&gt;. State the no-double-sell invariant and the separate idempotency requirement. Show the naive two-buyer race before introducing a conditional atomic transition.&lt;/p&gt;

&lt;p&gt;Then build the design in the order complexity appears: a durable hold, all-or-none multi-seat acquisition, identity-scoped expiry, payment ambiguity and late success, booking confirmation, outbox-driven ticket issuance, and reconciliation. Only after that introduce the waiting room, hot-event partitioning, and regional failover.&lt;/p&gt;

&lt;p&gt;A useful interviewer follow-up is: &lt;em&gt;“The payment succeeded, but the hold expired and somebody else now owns the seat. What happens?”&lt;/em&gt; The answer should not be “increase the TTL.” The answer is that the old booking cannot be confirmed against someone else's allocation; the late payment needs a defined refund/void or alternate-fulfillment path, and the system must reconcile the financial outcome.&lt;/p&gt;

&lt;p&gt;Another is: &lt;em&gt;“An expiry job for yesterday's hold runs against a seat that is held again today.”&lt;/em&gt; If the release checks only &lt;code&gt;state = HELD&lt;/code&gt;, the design is unsafe. If it checks the specific &lt;code&gt;hold_id&lt;/code&gt; and authoritative expiry/version in the same guarded transaction, the stale job is harmless.&lt;/p&gt;

&lt;p&gt;These scenarios distinguish an architecture that works in a diagram from one that remains correct when events arrive late, twice, or in the wrong order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The hardest part of ticket booking is not rendering a seat map or processing a payment. It is maintaining a consistent account of &lt;strong&gt;who owns scarce inventory&lt;/strong&gt; when the network, workers, customers, and payment providers do not move in lockstep.&lt;/p&gt;

&lt;p&gt;Atomic reservation chooses one winner. Durable, idempotent holds remember that winner. Identity-scoped expiry prevents stale cleanup from stealing the next winner's seat. Payment coordination refuses to mistake uncertainty for failure. The outbox and reconciliation paths make the system recoverable after partial success.&lt;/p&gt;

&lt;p&gt;That is the foundation of a ticket-booking system that can handle high demand without turning operational failures into double bookings.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Explore the complete visual and technical breakdown on SeeItFlow:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/ticketbooking/complete-design" rel="noopener noreferrer"&gt;Ticket Booking System — Complete Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/ticketbooking/visual-design-journey" rel="noopener noreferrer"&gt;Visual Design Journey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/ticketbooking/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/ticketbooking/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/ticketbooking/failure-scale-scenarios" rel="noopener noreferrer"&gt;Failure &amp;amp; Scale Scenarios&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/intermediate/ticketbooking/interview-perspective" rel="noopener noreferrer"&gt;Interview Perspective&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>backend</category>
      <category>distributed</category>
    </item>
    <item>
      <title>E-commerce System Design: Inventory, Checkout, Orders &amp; Scale</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Thu, 15 Oct 2026 09:43:14 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/e-commerce-system-design-inventory-checkout-orders-scale-2f4p</link>
      <guid>https://dev.to/mangeshmandlik/e-commerce-system-design-inventory-checkout-orders-scale-2f4p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faucm38dz257ez2aaf5ma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faucm38dz257ez2aaf5ma.png" alt="ecommerce final architecture" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A shopper adds the last available item to a cart. Another shopper does the same. Both click &lt;strong&gt;Place Order&lt;/strong&gt;. One payment times out; the other succeeds. Meanwhile, the product price changes, the fulfillment queue is delayed, and the checkout service restarts.&lt;/p&gt;

&lt;p&gt;A credible &lt;strong&gt;e-commerce system design&lt;/strong&gt; must explain what happens next—not merely draw boxes labeled Catalog, Cart, Order, and Payment.&lt;/p&gt;

&lt;p&gt;The central challenge is that an online store contains several workloads with different consistency requirements. Browsing and search benefit from caches and asynchronous indexing. Inventory reservations and order transitions require guarded writes. Payments involve external systems whose outcomes may remain unknown after a timeout. Fulfillment happens on a different timeline altogether.&lt;/p&gt;

&lt;p&gt;This walkthrough builds an Amazon-like &lt;strong&gt;online shopping platform architecture&lt;/strong&gt; for a general retailer. It is an illustrative design, &lt;strong&gt;not a claim about Amazon's internal implementation&lt;/strong&gt;. It emphasizes the decisions and failure modes that matter in production and in a system design interview.&lt;/p&gt;

&lt;p&gt;For the full interactive learning path, start with &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/complete-design" rel="noopener noreferrer"&gt;E-commerce Platform — Complete Design&lt;/a&gt; on SeeItFlow.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements: what are we actually building?
&lt;/h2&gt;

&lt;p&gt;Our baseline supports product browsing, keyword search and filters, SKU selection, guest and signed-in carts, checkout, inventory reservation, payment, order history, fulfillment tracking, cancellation, returns, and refunds.&lt;/p&gt;

&lt;p&gt;We will initially assume a single retailer selling physical goods, with a finite stock policy and an external payment service provider. Multiple sellers, warehouse selection, sophisticated promotion engines, fraud detection, and international tax calculation can be added as explicit extensions. A marketplace changes the ownership of offers, inventory, and fulfillment; it is not merely the same architecture with more users.&lt;/p&gt;

&lt;p&gt;Rather than assigning one latency or consistency target to the whole platform, specify them per workflow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Optimization goal&lt;/th&gt;
&lt;th&gt;Correctness boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog&lt;/td&gt;
&lt;td&gt;Low-latency, high-volume reads&lt;/td&gt;
&lt;td&gt;Product data can be briefly stale in display&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;Relevance, filtering, independent scaling&lt;/td&gt;
&lt;td&gt;Search index is a derived discovery view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart&lt;/td&gt;
&lt;td&gt;Fast mutable session state&lt;/td&gt;
&lt;td&gt;Cart does not grant a stock reservation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;Efficient display and quoting&lt;/td&gt;
&lt;td&gt;Checkout uses an accepted, versioned quote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory&lt;/td&gt;
&lt;td&gt;Safe concurrent reservations&lt;/td&gt;
&lt;td&gt;Allocation never exceeds the defined stock policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout and orders&lt;/td&gt;
&lt;td&gt;Recoverable workflow&lt;/td&gt;
&lt;td&gt;Durable identities and guarded transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment&lt;/td&gt;
&lt;td&gt;Safe external side effects&lt;/td&gt;
&lt;td&gt;Unknown outcome is not a decline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fulfillment&lt;/td&gt;
&lt;td&gt;Reliable asynchronous processing&lt;/td&gt;
&lt;td&gt;No duplicate fulfillment for the same operation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Availability and latency SLOs should be derived from business needs and measured load; numbers quoted in interviews are design assumptions, not universal industry guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Model products, SKUs, offers, and inventory separately
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Product&lt;/strong&gt; describes an item: title, category, brand, images, and attributes. A &lt;strong&gt;SKU&lt;/strong&gt; identifies a purchasable variant, such as a black T-shirt in size M. An &lt;strong&gt;Offer&lt;/strong&gt; describes the commercial terms under which a merchant sells a SKU, including currency, price, validity, and seller identity when applicable.&lt;/p&gt;

&lt;p&gt;Inventory is not a property of the abstract product. It belongs to the relevant stock pool, often keyed by SKU and fulfillment location. For a marketplace, seller-specific offers and stock ownership need additional modeling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product (product_id, title, attributes)
   ├── SKU A (sku_id, color=black, size=M)
   │      ├── Offer (seller, currency, price_version)
   │      └── Inventory (sku_id, location_id, on_hand, reserved)
   └── SKU B (sku_id, color=blue, size=L)
          └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An order must also preserve a snapshot of what the customer accepted: SKU, quantity, product description as needed, unit price, currency, discounts, taxes, shipping allocation, and totals. &lt;strong&gt;Never recalculate a historical order using today's catalog price.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This separation prevents frequent price and stock updates from requiring the same storage and cache policies as relatively stable catalog metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Catalog architecture: cache the browsing path
&lt;/h2&gt;

&lt;p&gt;A straightforward catalog read path uses a database plus a cache-aside layer. Images and other static media are served through a CDN.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6xpsxtalyc3cllfdte5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6xpsxtalyc3cllfdte5.png" alt="catalog architecture" width="800" height="289"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser ──&amp;gt; API ──&amp;gt; Catalog Service ──&amp;gt; Cache
                           │               │ miss
                           └──────────────&amp;gt; Catalog DB

Browser ──&amp;gt; CDN ──&amp;gt; Product images / static media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Popular products benefit from caching, but cache hit rate depends on popularity distribution, catalog size, TTL, and capacity. Images can be updated, so version asset URLs or invalidate CDN entries rather than assuming every image URL is immutable.&lt;/p&gt;

&lt;p&gt;Two operational details matter. First, a cache outage can suddenly redirect a large fraction of traffic to the database. Protect the origin with load shedding, circuit breakers, and capacity-aware fallbacks. Second, a hot entry expiring during a traffic spike can cause a &lt;strong&gt;cache stampede&lt;/strong&gt;. Single-flight request coalescing, TTL jitter, and carefully chosen stale-while-revalidate behavior help on read-only display data.&lt;/p&gt;

&lt;p&gt;A cached product page may show an approximate stock count or an older price. &lt;strong&gt;Neither value authorizes a reservation or a charge.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Search is a derived read model, not a second source of truth
&lt;/h2&gt;

&lt;p&gt;Product search needs full-text matching, ranking, filters, and potentially typo tolerance and autocomplete. A relational database's full-text search may be sufficient initially; a dedicated search index becomes valuable when search functionality or load justifies it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpp87fbnhxf0avlv9vrtz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpp87fbnhxf0avlv9vrtz.png" alt="product search" width="800" height="290"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Catalog DB ──&amp;gt; Durable change pipeline ──&amp;gt; Indexer ──&amp;gt; Search Index
                                                      │
Customer query ──&amp;gt; Search API ──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Index updates are asynchronous, so the search index can lag behind the catalog. Define an indexing-lag SLO and measure it. A product may briefly appear in search after being disabled or sold out; checkout must revalidate SKU status, accepted pricing, and inventory independently.&lt;/p&gt;

&lt;p&gt;This is a useful &lt;strong&gt;CQRS-style separation&lt;/strong&gt;: the transactional catalog is authoritative, while search is optimized for discovery. It does not mean every e-commerce application needs a full CQRS framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cart design: purchase intent is not inventory ownership
&lt;/h2&gt;

&lt;p&gt;A cart normally stores &lt;code&gt;sku_id&lt;/code&gt;, quantity, timestamps, and perhaps a displayed-price snapshot, keyed by user or guest session. A key-value store can fit frequent small updates, though a relational store is also viable.&lt;/p&gt;

&lt;p&gt;Adding an item to the cart generally &lt;strong&gt;does not reserve stock&lt;/strong&gt;. Otherwise abandoned carts could hold scarce inventory for long periods. Reservation at checkout is a sensible baseline for ordinary retail, while ticketing and limited drops may deliberately reserve earlier for a short, bounded window.&lt;/p&gt;

&lt;p&gt;Guest-cart merging after login needs an explicit policy: combine quantities, prefer one cart, or ask the shopper. It should not silently create invalid quantities or imply that a saved cart guarantees availability.&lt;/p&gt;

&lt;p&gt;At checkout, revalidate every SKU's purchasability, quantity, price, and availability. Cart state is a starting point, not authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Pricing: distinguish display price, checkout quote, and order price
&lt;/h2&gt;

&lt;p&gt;One subtle failure is treating the price rendered on a product page as the price the payment service must charge.&lt;/p&gt;

&lt;p&gt;There are three distinct values:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Display price:&lt;/strong&gt; a potentially cached browsing value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout quote:&lt;/strong&gt; the authoritative commercial decision for the current items, currency, applicable discounts, tax and shipping policy, with a version and/or expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order line price:&lt;/strong&gt; the accepted terms snapshotted onto the order for history and financial reconciliation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An authoritative quote does not necessarily mean a direct database read on every request. A pricing service or appropriately versioned and invalidated cache can provide it. What matters is that the checkout decision obeys the pricing policy and quote validity rules.&lt;/p&gt;

&lt;p&gt;If the price changes between browsing and checkout, show the updated terms and obtain acceptance as required by the product policy. Do not silently charge a different amount. Once accepted, ensure the payment amount and currency correspond to the persisted quote and order snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Inventory reservation: the last-item race
&lt;/h2&gt;

&lt;p&gt;Suppose there is one sellable unit of SKU-42. Two shoppers attempt to buy it simultaneously. A read-then-write sequence without concurrency control is unsafe: both requests may observe one available unit and both proceed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0k2jhoqk3sam413rrjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0k2jhoqk3sam413rrjh.png" alt="inventory reservation" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a simple stock pool, define:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;available_to_sell = on_hand - reserved - safety_stock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simplified model. Production inventory may also distinguish damaged, allocated, in-transit, and backordered units. Some retailers intentionally permit backorders; the real invariant is that reservations do not exceed &lt;strong&gt;the allocation policy&lt;/strong&gt; for the SKU and stock pool.&lt;/p&gt;

&lt;p&gt;A relational implementation can reserve atomically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sku_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sku_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;location_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;on_hand&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;safety_stock&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Proceed only if exactly one inventory row was updated.&lt;/span&gt;
&lt;span class="c1"&gt;-- Insert a uniquely identified reservation in the SAME transaction.&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkout_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;checkout_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sku_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ACTIVE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application must roll back the transaction if the conditional update affects no row, if the reservation insert fails, or if any invariant check fails. A retry with the same reservation identity must return the existing reservation rather than incrementing &lt;code&gt;reserved&lt;/code&gt; again. That requires a uniqueness constraint and idempotent request handling; the SQL sketch alone does not implement the complete retry protocol.&lt;/p&gt;

&lt;p&gt;Other valid mechanisms include optimistic version checks, row locks, strongly consistent conditional writes, and serialized per-SKU processing. A distributed lock by itself is not a substitute for an authoritative conditional write.&lt;/p&gt;

&lt;p&gt;A reservation is a &lt;strong&gt;durable entity&lt;/strong&gt;, not merely a changed counter. Typical states are &lt;code&gt;ACTIVE&lt;/code&gt;, &lt;code&gt;COMMITTED&lt;/code&gt;, &lt;code&gt;RELEASED&lt;/code&gt;, and &lt;code&gt;EXPIRED&lt;/code&gt;. Every transition must be idempotent and guarded so that commit and expiry cannot both win.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Reservation expiry must survive crashes
&lt;/h2&gt;

&lt;p&gt;An abandoned checkout should eventually release its held units. Store &lt;code&gt;expires_at&lt;/code&gt; durably and enforce expiry through a recoverable mechanism: indexed database scans, a durable delayed task, or a workflow timer.&lt;/p&gt;

&lt;p&gt;Do not rely solely on an in-memory timer or cache-key expiry notification. Those can be missed. The expiry worker should perform a conditional transition from &lt;code&gt;ACTIVE&lt;/code&gt; to &lt;code&gt;EXPIRED&lt;/code&gt;, update the stock accounting consistently, and tolerate retries.&lt;/p&gt;

&lt;p&gt;There is a critical exception: &lt;strong&gt;a payment timeout does not prove payment failure&lt;/strong&gt;. If the payment outcome is unknown near reservation expiry, the business needs a bounded policy—extend the hold where possible, or let it expire with a defined late-success allocation-or-refund procedure. Do not hold inventory forever, but do not casually release it on a network timeout either.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Checkout is a durable saga, not one global transaction
&lt;/h2&gt;

&lt;p&gt;Inventory, orders, and an external payment provider cannot generally participate in one shared ACID transaction. A checkout therefore coordinates local transactions and external effects through a durable &lt;strong&gt;saga&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5y9en2cq0ruvqw94q9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5y9en2cq0ruvqw94q9u.png" alt="checkout sequence" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A defensible baseline sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer
   │ Place Order (idempotency key)
   ▼
Checkout / Order Service
   │ 1. Create or reuse durable PENDING checkout/order
   │ 2. Persist accepted quote and line-item snapshot
   ▼
Inventory Service
   │ 3. Reserve stock idempotently
   ▼
Payment Service / Provider
   │ 4. Initiate or resume the same logical payment
   │    ├── confirmed success
   │    ├── definitive failure
   │    └── pending / unknown
   ▼
Checkout Orchestrator
   │ 5. Resolve payment and reservation conditions
   │ 6. Commit reservation and confirm order via
   │    guarded, recoverable cross-service workflow
   ▼
Order DB + Transactional Outbox
   │ 7. Persist CONFIRMED and order.confirmed together
   ▼
Event Broker ──&amp;gt; Fulfillment / Notification / Analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a &lt;strong&gt;logical workflow&lt;/strong&gt;, not one atomic operation spanning the inventory and order databases. If reservation commit succeeds but order confirmation fails, the orchestrator must resume or reconcile that intermediate state. Fulfillment must not begin merely because a payment response looked successful; it begins from a durable, valid confirmed order according to the platform's policy.&lt;/p&gt;

&lt;p&gt;The checkout idempotency key identifies one logical attempt and maps to a durable checkout/order record. Store a request fingerprint so reusing the same key for a different cart or quote is rejected rather than silently treated as the same purchase. Separate &lt;code&gt;checkout_id&lt;/code&gt;, &lt;code&gt;order_id&lt;/code&gt;, &lt;code&gt;reservation_id&lt;/code&gt;, and payment identifiers remain linked but should not be conflated.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if the orchestrator crashes?
&lt;/h3&gt;

&lt;p&gt;Record durable workflow progress and give every step a stable business idempotency identity. When execution resumes, query or reconcile the previous step's outcome before deciding whether to repeat an external side effect. &lt;strong&gt;Idempotency requires cooperation from each boundary&lt;/strong&gt;, especially the payment provider; a local database key cannot by itself prevent an unrelated provider charge.&lt;/p&gt;

&lt;p&gt;A saga's compensation is not rollback. Releasing stock, voiding an authorization, issuing a refund, or cancelling an order are separate operations that can themselves fail and need retries and reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Payment success, failure, and unknown are different outcomes
&lt;/h2&gt;

&lt;p&gt;An e-commerce checkout should integrate with a dedicated payment component instead of reimplementing payment-network internals.&lt;/p&gt;

&lt;p&gt;A definitive provider decline can trigger an idempotent reservation release and a failed checkout state. A payment still processing should remain pending. A network timeout, dropped response, or delayed webhook leaves an &lt;strong&gt;unknown outcome&lt;/strong&gt;, not a definitive failure.&lt;/p&gt;

&lt;p&gt;Use a stable provider-side idempotency mechanism where supported, authenticated webhook processing, provider status queries, and reconciliation. Provider capabilities and guarantees vary by payment method and API, so avoid claiming universal exactly-once charging.&lt;/p&gt;

&lt;p&gt;Also distinguish &lt;strong&gt;authorization&lt;/strong&gt;, &lt;strong&gt;capture&lt;/strong&gt;, &lt;strong&gt;refund&lt;/strong&gt;, and &lt;strong&gt;settlement&lt;/strong&gt;. They are different financial events. Some integrations authorize before capture; others capture immediately or complete asynchronously. An order may be confirmed according to a defined payment policy without claiming that bank settlement has already occurred.&lt;/p&gt;

&lt;p&gt;The detailed payment state machine belongs in &lt;a href="https://seeitflow.com/system-design/intermediate/payment/complete-design" rel="noopener noreferrer"&gt;Payment System Design on SeeItFlow&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Orders, payments, and fulfillment have separate state machines
&lt;/h2&gt;

&lt;p&gt;A single &lt;code&gt;order_complete&lt;/code&gt; flag cannot represent the real lifecycle.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Illustrative states&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Order&lt;/td&gt;
&lt;td&gt;PENDING, CONFIRMED, CANCELLED, COMPLETED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment&lt;/td&gt;
&lt;td&gt;PENDING, UNKNOWN, AUTHORIZED, CAPTURED, FAILED, REFUNDED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reservation&lt;/td&gt;
&lt;td&gt;ACTIVE, COMMITTED, RELEASED, EXPIRED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fulfillment&lt;/td&gt;
&lt;td&gt;UNALLOCATED, ALLOCATED, PICKING, PACKED, SHIPPED, DELIVERED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These examples are not one universal state model: providers and merchants may need partial captures, multiple payment attempts, partial refunds, and per-line or per-shipment fulfillment states.&lt;/p&gt;

&lt;p&gt;An order can be confirmed but not shipped. One order can generate several shipments. A cancellation can be accepted while the refund is still pending. Keep each domain's ownership and transitions explicit, and derive the customer-facing status from the combination rather than overwriting one boolean.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Transactional outbox: don't lose the fulfillment event
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Order DB commits CONFIRMED.
2. Process crashes before publishing order.confirmed.
3. Fulfillment never hears about the order.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publishing to a message broker immediately after the database commit creates a &lt;strong&gt;dual-write gap&lt;/strong&gt;. The transactional outbox addresses it by inserting the order transition and an outbox event in the &lt;strong&gt;same local database transaction&lt;/strong&gt;. A relay later publishes the event and retries on failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order DB transaction:
  UPDATE orders SET status = 'CONFIRMED' ...
  INSERT INTO outbox (event_id, order_id, event_type, payload) ...
  COMMIT

Outbox relay ──&amp;gt; Broker ──&amp;gt; Fulfillment consumer
                           ├── Notification consumer
                           └── Analytics consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outbox protects the durability of the event record, but it does &lt;strong&gt;not&lt;/strong&gt; guarantee exactly-once delivery. A relay can publish successfully and crash before recording the acknowledgment, then publish again. Consumers must deduplicate or apply operations idempotently using stable business identities.&lt;/p&gt;

&lt;p&gt;For fulfillment, the uniqueness boundary must match the real operation—such as a fulfillment request or shipment operation per relevant order lines—not blindly &lt;code&gt;order_id&lt;/code&gt; if partial shipments are supported. A carrier-label API also needs idempotency or an external-result reconciliation strategy, because database deduplication alone cannot undo a duplicate external shipment request.&lt;/p&gt;

&lt;p&gt;Consumers must handle out-of-order events using order versions and guarded transitions where ordering is not guaranteed.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Fulfillment, cancellation, returns, and refunds
&lt;/h2&gt;

&lt;p&gt;After order confirmation, fulfillment allocates a location, picks and packs goods, creates shipments, and tracks delivery. This workflow should normally be asynchronous so a slow warehouse does not block the customer's checkout response.&lt;/p&gt;

&lt;p&gt;Cancellation eligibility depends on current fulfillment state, not the state the shopper saw seconds earlier. A cancellation request racing with shipment creation must use guarded transitions and coordination with the fulfillment owner. If the shipment has passed the cancellable point, the request may need to become a return.&lt;/p&gt;

&lt;p&gt;Returned items should not automatically be marked sellable: inspection, damage, and disposition policy may apply. Similarly, &lt;strong&gt;order cancellation is not refund completion&lt;/strong&gt;. Refunds can be pending, partially completed, failed, or ambiguous. The customer interface and reconciliation process must reflect those distinct facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Flash sales: one hot SKU defeats naive horizontal scaling
&lt;/h2&gt;

&lt;p&gt;Imagine 100,000 shoppers competing for 1,000 units of the same SKU. A CDN can absorb image and catalog traffic, and stateless API instances can scale horizontally. But every successful stock reservation must still respect one authoritative inventory allocation boundary.&lt;/p&gt;

&lt;p&gt;Adding more API servers does not eliminate contention on that hot row, partition, or reservation authority.&lt;/p&gt;

&lt;p&gt;Possible mitigations include bounded concurrency, rate limits, exponential backoff with jitter where retries are appropriate, early sold-out rejection, and—when fairness or extreme demand requires it—a waiting room or admission tokens. At the highest contention levels, a serialized per-SKU reservation path or carefully designed inventory preallocation can help, but adds operational complexity and reconciliation needs.&lt;/p&gt;

&lt;p&gt;A waiting room is not a correctness guarantee by itself. The final stock allocation must still be guarded. Partitioning by SKU spreads &lt;strong&gt;different&lt;/strong&gt; SKUs across shards; it does not automatically distribute one extremely popular SKU.&lt;/p&gt;

&lt;p&gt;For a deeper failure walkthrough, see &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/failure-scale-scenarios" rel="noopener noreferrer"&gt;E-commerce Failure &amp;amp; Scale Scenarios&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Failure scenarios worth designing before launch
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Unsafe reaction&lt;/th&gt;
&lt;th&gt;Safer design&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Two buyers request last unit&lt;/td&gt;
&lt;td&gt;Read stock then decrement separately&lt;/td&gt;
&lt;td&gt;Atomic conditional reservation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate Place Order request&lt;/td&gt;
&lt;td&gt;Create a new order/payment each time&lt;/td&gt;
&lt;td&gt;Durable checkout identity and fingerprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reservation succeeds; response lost&lt;/td&gt;
&lt;td&gt;Reserve again&lt;/td&gt;
&lt;td&gt;Query/reuse same reservation identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment request times out&lt;/td&gt;
&lt;td&gt;Treat as declined and release stock&lt;/td&gt;
&lt;td&gt;Mark unknown; query/webhook/reconcile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment succeeds; order remains pending&lt;/td&gt;
&lt;td&gt;Forget the attempt&lt;/td&gt;
&lt;td&gt;Recover from durable checkout/payment linkage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reservation expires during confirmation&lt;/td&gt;
&lt;td&gt;Commit and release concurrently&lt;/td&gt;
&lt;td&gt;Guarded single-winner state transition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price changes during checkout&lt;/td&gt;
&lt;td&gt;Charge whatever cart displayed&lt;/td&gt;
&lt;td&gt;Revalidate and obtain accepted quote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order commits; publish fails&lt;/td&gt;
&lt;td&gt;Lose fulfillment event&lt;/td&gt;
&lt;td&gt;Transactional outbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fulfillment event redelivered&lt;/td&gt;
&lt;td&gt;Create duplicate shipment&lt;/td&gt;
&lt;td&gt;Idempotent fulfillment operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancellation races with shipping&lt;/td&gt;
&lt;td&gt;Cancel based on stale display&lt;/td&gt;
&lt;td&gt;Guarded eligibility and return fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory database unavailable&lt;/td&gt;
&lt;td&gt;Trust cached stock&lt;/td&gt;
&lt;td&gt;Pause/fail reservation safely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region fails during checkout&lt;/td&gt;
&lt;td&gt;Allow both regions to write same stock&lt;/td&gt;
&lt;td&gt;Explicit authority, fencing, recovery and reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Regional failover deserves particular care. Replication alone does not preserve idempotency or prevent two regions from accepting conflicting reservations. Define write ownership, promotion/fencing, recovery-point objectives, and reconciliation before enabling failover. Active-active inventory is an advanced design, not a default checkbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. Reconciliation catches what retries cannot
&lt;/h2&gt;

&lt;p&gt;Even careful idempotency and retry handling cannot guarantee that every cross-system workflow will resolve without intervention.&lt;/p&gt;

&lt;p&gt;A reconciliation job should look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment succeeded, but order remains pending.&lt;/li&gt;
&lt;li&gt;Order confirmed without the expected committed reservation.&lt;/li&gt;
&lt;li&gt;Reservation committed for a checkout that never confirmed.&lt;/li&gt;
&lt;li&gt;Reservation expired while the payment later succeeded.&lt;/li&gt;
&lt;li&gt;Outbox event stuck beyond its expected publication lag.&lt;/li&gt;
&lt;li&gt;Duplicate or missing fulfillment operations.&lt;/li&gt;
&lt;li&gt;Refund expected after cancellation but not completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run bounded, indexed scans or event-driven exception checks; track discrepancy age and severity. Repairs must be idempotent, logged, and safe to rerun. Do &lt;strong&gt;not&lt;/strong&gt; automatically recreate stock allocations, issue refunds, or create shipments whenever the true physical or financial outcome is uncertain. Some cases require quarantine and human review.&lt;/p&gt;

&lt;p&gt;Reconciliation is a correctness mechanism, not an excuse to ignore broken invariants on the live request path.&lt;/p&gt;

&lt;h2&gt;
  
  
  17. Architecture decisions and trade-offs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Reasonable baseline&lt;/th&gt;
&lt;th&gt;When to reconsider&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog search&lt;/td&gt;
&lt;td&gt;DB search initially, dedicated index when justified&lt;/td&gt;
&lt;td&gt;Complex relevance, facets, high search load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product display&lt;/td&gt;
&lt;td&gt;Cache/CDN&lt;/td&gt;
&lt;td&gt;Personalization or strict freshness needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart stock hold&lt;/td&gt;
&lt;td&gt;No reservation until checkout&lt;/td&gt;
&lt;td&gt;Scarce time-boxed inventory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory concurrency&lt;/td&gt;
&lt;td&gt;Conditional atomic write&lt;/td&gt;
&lt;td&gt;Extreme hot-SKU contention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reserve vs. pay&lt;/td&gt;
&lt;td&gt;Reserve before payment&lt;/td&gt;
&lt;td&gt;Authorization-first or domain-specific flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout coordination&lt;/td&gt;
&lt;td&gt;Durable orchestrated saga&lt;/td&gt;
&lt;td&gt;Simpler local workflow or event-driven choreography&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order timing&lt;/td&gt;
&lt;td&gt;Create durable pending identity before external payment&lt;/td&gt;
&lt;td&gt;Alternative models with equivalent recovery anchors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downstream delivery&lt;/td&gt;
&lt;td&gt;Outbox and idempotent consumers&lt;/td&gt;
&lt;td&gt;Simpler noncritical events with accepted loss risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Modular monolith can be sufficient&lt;/td&gt;
&lt;td&gt;Independent scale, team ownership, release cadence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are design choices, not a mandate to deploy a dozen microservices. Clean domain boundaries can exist inside a modular monolith. Independent services become worthwhile when deployment ownership, scaling, or isolation needs justify the operational cost.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/tradeoffs-decisions" rel="noopener noreferrer"&gt;E-commerce Trade-offs &amp;amp; Decisions&lt;/a&gt; for the full comparisons and &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt; for implementation mechanisms.&lt;/p&gt;

&lt;h2&gt;
  
  
  18. How to answer e-commerce system design interview questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you prevent overselling?&lt;/strong&gt; Use an authoritative atomic reservation per SKU/stock pool, a durable reservation identity, and guarded commit/release/expiry transitions. The display count can be stale; the reservation decision cannot be based on that stale count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if payment succeeds but the order service crashes?&lt;/strong&gt; Persist a checkout/order recovery identity before the payment attempt, resolve the provider outcome using the linked payment identity, and resume confirmation or apply the predefined recovery/refund policy. Never blindly charge again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not use one database transaction for checkout?&lt;/strong&gt; An external payment provider cannot generally join the inventory/order database transaction. Use local commits coordinated by a durable saga with idempotent compensation and reconciliation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do you need an outbox if you already use Kafka or another broker?&lt;/strong&gt; Because the order database commit and the broker publish are separate writes. The outbox closes that local dual-write durability gap; it does not eliminate duplicate deliveries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if 100,000 people buy the last 1,000 units?&lt;/strong&gt; Protect admission, serialize or guard the inventory allocation boundary, fail fast on sold-out inventory, and keep payment calls downstream of successful reservation. More stateless instances alone do not solve one hot stock record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you cache order status?&lt;/strong&gt; Yes, a read-only display view can be cached with clear staleness semantics. Order-state mutations still require authoritative guarded transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changes for a marketplace?&lt;/strong&gt; Seller-scoped offers, seller/location inventory ownership, split fulfillment, merchant settlement, and additional policy boundaries. Do not assume one product has one global price or one global stock pool.&lt;/p&gt;

&lt;p&gt;For more interviewer follow-ups, see &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/interview-perspective" rel="noopener noreferrer"&gt;E-commerce Interview Perspective&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  19. Final architecture and takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faucm38dz257ez2aaf5ma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faucm38dz257ez2aaf5ma.png" alt="ecommerce final architecture" width="800" height="413"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                           Customers
                               │
                        API / Gateway
                               │
         ┌─────────────────────┼──────────────────────┐
         │                     │                      │
   Catalog Service       Search Service         Cart Service
     │       │                 │                      │
 Catalog DB  Cache         Search Index            Cart Store
     │                         ▲
     └── Change pipeline ──────┘

                      Checkout / Order
                               │
                   Durable Saga / Order DB
                    │          │         │
               Pricing      Inventory   Payment Service
               Quote        Service     │
                             │          Payment Provider
                       Reservation DB
                               │
                       Order CONFIRMED
                               │
                    Order DB + Outbox
                               │
                         Event Relay
                               │
                            Broker
                         /     |      \
                  Fulfillment Notify  Analytics

         Reconciliation checks order, inventory,
         payment, outbox, and fulfillment outcomes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important lesson in &lt;strong&gt;e-commerce system design&lt;/strong&gt; is not to make every component strongly consistent. It is to &lt;strong&gt;identify which decisions change money, stock, or contractual order state—and protect those boundaries precisely&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Cache catalog and search where staleness is tolerable. Keep carts as intent, not inventory ownership. Quote prices authoritatively. Reserve stock atomically. Make checkout durable and idempotent across retries. Treat payment timeouts as ambiguous. Publish confirmed orders through an outbox, make downstream consumers idempotent, and reconcile the gaps that distributed failures leave behind.&lt;/p&gt;

&lt;p&gt;The result is an architecture that can scale browsing aggressively without trading away correctness at checkout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the complete SeeItFlow learning path:&lt;/strong&gt; &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/complete-design" rel="noopener noreferrer"&gt;Complete Design&lt;/a&gt; · &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt; · &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions&lt;/a&gt; · &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/failure-scale-scenarios" rel="noopener noreferrer"&gt;Failure &amp;amp; Scale Scenarios&lt;/a&gt; · &lt;a href="https://seeitflow.com/system-design/intermediate/ecommerce/interview-perspective" rel="noopener noreferrer"&gt;Interview Perspective&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>backend</category>
      <category>distributed</category>
    </item>
    <item>
      <title>Payment System Design: Idempotency, Webhooks, Ledger &amp; Failures</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Thu, 15 Oct 2026 05:45:49 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/payment-system-design-idempotency-webhooks-ledger-failures-3ck0</link>
      <guid>https://dev.to/mangeshmandlik/payment-system-design-idempotency-webhooks-ledger-failures-3ck0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frj1s4e7k1sfcel4p5y90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frj1s4e7k1sfcel4p5y90.png" alt="final architecture" width="799" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A customer taps &lt;strong&gt;Pay&lt;/strong&gt;. The screen spins, the request times out, and the customer taps again.&lt;/p&gt;

&lt;p&gt;Did the first payment fail? Did the provider charge the customer but lose the response? Can the second tap safely retry? And if the payment succeeded, what happens when the order service never receives the confirmation?&lt;/p&gt;

&lt;p&gt;These are the questions that make &lt;strong&gt;payment system design&lt;/strong&gt; fundamentally different from a typical CRUD service. The difficult part is not sending a request to a payment gateway. It is preserving financial correctness when different systems observe different outcomes.&lt;/p&gt;

&lt;p&gt;This article designs a one-time payment orchestration system, starting with a simple flow and evolving it through the failures that justify each architectural component. It covers idempotency, payment attempts, webhooks, transactional outbox, refunds, financial ledgers, reconciliation, provider outages, and scaling.&lt;/p&gt;

&lt;p&gt;For an interactive walkthrough, see &lt;a href="https://seeitflow.com/system-design/intermediate/payment/complete-design" rel="noopener noreferrer"&gt;Payment System — Complete Design on SeeItFlow&lt;/a&gt; and the &lt;a href="https://seeitflow.com/system-design/intermediate/payment/visual-design-journey" rel="noopener noreferrer"&gt;Visual Design Journey&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the payment system we are designing
&lt;/h2&gt;

&lt;p&gt;We are building a service that accepts a customer's payment intent, coordinates with an external payment service provider (PSP), records the result, and reliably informs downstream systems.&lt;/p&gt;

&lt;p&gt;It must support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating and retrieving a logical payment.&lt;/li&gt;
&lt;li&gt;Preventing accidental duplicate payments from client retries and concurrent requests.&lt;/li&gt;
&lt;li&gt;Handling immediate, pending, and ambiguous provider outcomes.&lt;/li&gt;
&lt;li&gt;Receiving authenticated provider webhooks.&lt;/li&gt;
&lt;li&gt;Reconciling local records with provider records.&lt;/li&gt;
&lt;li&gt;Issuing full or partial refunds when supported.&lt;/li&gt;
&lt;li&gt;Recording an auditable history of financial operations.&lt;/li&gt;
&lt;li&gt;Triggering downstream actions without coupling their availability to the payment request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The baseline assumes one-time payments through an external provider. Subscription billing, card-network authorization internals, tax, FX, fraud scoring, and merchant settlement infrastructure are separate systems or extensions. Actual capabilities depend on the provider, payment method, country, and merchant configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core invariant:&lt;/strong&gt; retries of one logical payment must not create additional independent charges. This is a design objective enforced by several layers, not an unconditional guarantee that an external financial system can never produce a duplicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Start with the simplest payment flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer
   |
   v
Payment API --&amp;gt; Payment Database
   |
   v
Payment Provider (PSP)
   |
   +--&amp;gt; synchronous result or pending/next action
   |
   +--&amp;gt; later webhook / status lookup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7roc0hzl3gsp7s3kkrwz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7roc0hzl3gsp7s3kkrwz.png" alt="simple payment flow" width="799" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A safe basic flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticate and validate the request, including the merchant/order, amount, and currency.&lt;/li&gt;
&lt;li&gt;Atomically create or find the existing logical payment associated with its idempotency key.&lt;/li&gt;
&lt;li&gt;Persist a payment attempt and its stable provider-facing identity &lt;strong&gt;before&lt;/strong&gt; contacting the provider.&lt;/li&gt;
&lt;li&gt;Send the operation through the provider adapter, using the provider's documented idempotency mechanism when available.&lt;/li&gt;
&lt;li&gt;Persist the outcome actually known: succeeded, definitively failed, requires customer action, pending, or unresolved.&lt;/li&gt;
&lt;li&gt;Return the current state and a stable payment identifier. Resolve nonterminal outcomes through subsequent provider messages and reconciliation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The provider never writes directly to our database. Our service processes the provider's API response or webhook and performs its own guarded database updates.&lt;/p&gt;

&lt;p&gt;The payment API should not tell the customer &lt;strong&gt;“paid”&lt;/strong&gt; merely because the provider accepted an asynchronous request. Accepted, authorized, captured, and settled can represent different events.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Model payment intent separately from payment attempts
&lt;/h2&gt;

&lt;p&gt;A single payment is a business operation: for example, paying ₹1,500 for order &lt;code&gt;order_847&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;attempt&lt;/strong&gt; is a particular interaction with a provider to achieve that operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment: pay_847 (INR 150000 minor units)
  |
  +-- Attempt A: Provider X, definitive decline
  |
  +-- Attempt B: Provider X, successful charge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second attempt is permitted only after the system knows the first cannot also result in a charge, or after an explicitly designed recovery process. A timeout is &lt;em&gt;not&lt;/em&gt; enough evidence to start another independent attempt.&lt;/p&gt;

&lt;p&gt;A conceptual schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount_minor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;state_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;idempotency_records&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payment_attempts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempt_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact constraints depend on the business domain. If one order may legitimately have multiple payments, do &lt;strong&gt;not&lt;/strong&gt; introduce a simplistic globally unique &lt;code&gt;order_id&lt;/code&gt; constraint. Conversely, where the business requires only one successful capture for an order, enforce that invariant explicitly at the appropriate transactional boundary.&lt;/p&gt;

&lt;p&gt;Use integer minor units or fixed-precision decimal for money, with an explicit currency. Do not use binary floating point for authoritative monetary calculations, and do not assume all currencies have two fractional digits.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Payment state is a state machine, not &lt;code&gt;paid = true&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A simplified flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INITIATED --&amp;gt; PROCESSING --&amp;gt; SUCCEEDED
                  |             \
                  |              --&amp;gt; REFUND operation (separate lifecycle)
                  +--&amp;gt; FAILED (definitive)
                  +--&amp;gt; UNRESOLVED (outcome unknown)
                  +--&amp;gt; REQUIRES_ACTION (where supported)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fki3gu8a27bet1qgo6vo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fki3gu8a27bet1qgo6vo2.png" alt="payment state" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are &lt;strong&gt;internal illustrative states&lt;/strong&gt;, not a universal provider vocabulary. A real design may also distinguish authorization, capture, cancellation/void, expiry, partial capture, reversal, and partial refund. Some of these belong to separate operation-level state machines rather than a single overloaded payment status.&lt;/p&gt;

&lt;p&gt;State changes must be conditional. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;current_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;state_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state_version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;current_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;expected_state&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;state_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;expected_version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A zero-row update means the caller must reload and evaluate the current state; it does not automatically mean the provider event is wrong. Transition rules, provider event identity, and, where available, provider sequence/version information help avoid applying stale events.&lt;/p&gt;

&lt;p&gt;A delayed &lt;code&gt;PROCESSING&lt;/code&gt; webhook must not regress a confirmed successful capture. But &lt;strong&gt;refunds, reversals, and chargebacks are new financial facts&lt;/strong&gt;, not simply stale messages to discard because an earlier payment succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Idempotency: protect the customer from duplicate requests
&lt;/h2&gt;

&lt;p&gt;Suppose the client submits the same payment twice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /payments
Idempotency-Key: checkout-847-v1

{"order_id":"order_847","amount_minor":150000,"currency":"INR"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first request creates a durable payment. The second, using the same key and identical meaningful parameters, resolves to that same payment rather than creating a new independent charge.&lt;/p&gt;

&lt;p&gt;The implementation needs four pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A durable uniqueness constraint.&lt;/strong&gt; Scope the key to the appropriate merchant/account and operation. An atomic insert or equivalent transaction arbitrates concurrent requests. A Redis lookup or distributed lock alone is not sufficient: cache eviction, lock expiry, and crashes can erase the only deduplication signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A request fingerprint.&lt;/strong&gt; The same key with a different amount, currency, order, or operation is a conflict, not a request to charge a new amount. Reject it explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In-progress behavior.&lt;/strong&gt; If the original request is still running, return its current state, a pending response, or wait for a bounded period. Do not let the duplicate independently call the provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A separate provider-side idempotency boundary.&lt;/strong&gt; Retrying the &lt;em&gt;same provider attempt&lt;/em&gt; should reuse the same provider operation identity if that provider documents safe idempotent retries. Local uniqueness prevents duplicate logical records; provider idempotency helps prevent duplicate external effects. Neither mechanism substitutes for the other.&lt;/p&gt;

&lt;p&gt;Provider idempotency retention periods and exact replay semantics differ. Do not assume a provider remembers a key forever. Keep durable local business identity and reconciliation records for the periods required by the product and applicable obligations.&lt;/p&gt;

&lt;p&gt;Also, an idempotency key cannot deduplicate two logically identical requests if a buggy client generates a &lt;strong&gt;new key on every retry&lt;/strong&gt;. Where required, supplement request-level keys with a stable order/payment-intent invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The most important failure: provider timeout
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment Service             Provider
      |                        |
      |--- charge attempt ----&amp;gt;|
      |                        |  charge succeeds
      |&amp;lt;--- response lost ------X
      |
      |  local outcome: UNKNOWN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ovlpwjwbt45dld6s6c7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ovlpwjwbt45dld6s6c7.png" alt="timeout problem" width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The timeout says only that our service did not receive a conclusive response. The request might never have arrived, might still be processing, might have failed, or might already have succeeded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never equate a timeout with a declined payment.&lt;/strong&gt; And never automatically create a new attempt with a new provider identity because the old attempt timed out.&lt;/p&gt;

&lt;p&gt;A safe resolution strategy depends on the provider contract:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query the provider using the stable attempt/reference ID, where supported.&lt;/li&gt;
&lt;li&gt;Retry the &lt;em&gt;same&lt;/em&gt; provider operation with the &lt;em&gt;same&lt;/em&gt; idempotency identity only if the provider documents that as safe.&lt;/li&gt;
&lt;li&gt;Accept an authenticated webhook reporting the outcome.&lt;/li&gt;
&lt;li&gt;Escalate unresolved attempts to operational reconciliation and, when necessary, financial reconciliation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These channels can operate in parallel or in a different order; the provider's documented semantics determine which is safest. A provider API status response may itself be provisional, so do not treat every response as a final settlement fact.&lt;/p&gt;

&lt;p&gt;If the process crashes after the provider succeeds but before local &lt;code&gt;SUCCEEDED&lt;/code&gt; is committed, the persisted attempt and provider reference make later recovery possible. If the local success commit happens but the API response is lost, a retry with the same client key returns the existing payment.&lt;/p&gt;

&lt;p&gt;This is why durable identity must exist &lt;strong&gt;before&lt;/strong&gt; the external side effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Webhooks are necessary, but they are not a single source of guaranteed truth
&lt;/h2&gt;

&lt;p&gt;Many payment methods finish asynchronously. The provider notifies us later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider --&amp;gt; Webhook Endpoint --&amp;gt; Durable Inbox --&amp;gt; Worker
                                               |
                                               v
                                        Payment Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A robust webhook handler:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verifies the provider's documented authentication/signature mechanism against the exact bytes and headers required by that mechanism; checks timestamp/replay protection where supported.&lt;/li&gt;
&lt;li&gt;Durably records the incoming event or equivalent recoverable work item.&lt;/li&gt;
&lt;li&gt;Acknowledges according to the provider's retry contract.&lt;/li&gt;
&lt;li&gt;Processes it asynchronously and idempotently.&lt;/li&gt;
&lt;li&gt;Applies a guarded state transition and records any consequential event or ledger posting with its own deduplication key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not assume all providers sign the same payload or use the same event-ID semantics. A delivery ID is not always the same as a business-event ID; deduplication should use the provider's documented stable identifier, plus the underlying operation identity where needed.&lt;/p&gt;

&lt;p&gt;Expect duplicate delivery, out-of-order events, missing webhooks, and messages arriving after a status query has already resolved the payment. A signature-valid webhook also does not eliminate the need to check that the event belongs to the expected merchant, payment, amount, currency, and operation.&lt;/p&gt;

&lt;p&gt;A webhook for an unknown provider transaction should be investigated or quarantined rather than blindly creating a successful payment record.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. The database-to-queue dual-write problem
&lt;/h2&gt;

&lt;p&gt;Once a payment is confirmed, the system may need to release an order, grant access, notify the customer, and update financial records.&lt;/p&gt;

&lt;p&gt;A naïve implementation does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. UPDATE payments SET state = 'SUCCEEDED'
2. PUBLISH payment.succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What if the database commit succeeds and the process crashes before publishing? The payment is recorded, but the order service never hears about it.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;transactional outbox&lt;/strong&gt; solves that gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One local database transaction:
  - apply guarded payment state transition
  - insert outbox event with stable event_id
Commit

Outbox relay --&amp;gt; Message Broker --&amp;gt; Consumers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relay may publish the same event twice if it crashes after broker acknowledgement but before recording publication. Therefore the outbox supports reliable &lt;strong&gt;at-least-once delivery&lt;/strong&gt;, not magical end-to-end exactly-once effects.&lt;/p&gt;

&lt;p&gt;Each consumer needs its own idempotent business operation. For example, order fulfillment can enforce uniqueness on the payment/fulfillment operation it has already applied. Deduplicating by event ID alone may be insufficient if different event IDs represent the same underlying business action.&lt;/p&gt;

&lt;p&gt;An important accounting design choice: if the authoritative ledger and payment state live in the &lt;strong&gt;same transactional database&lt;/strong&gt;, write both in the same local transaction when appropriate. If ledger posting is asynchronous, explicitly monitor and reconcile the gap between payment confirmation and ledger posting; do not imply the outbox itself makes the ledger instantly consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Payment state, ledger, and settlement are different
&lt;/h2&gt;

&lt;p&gt;These concepts are often conflated in system design interviews.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;Typical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payment state&lt;/td&gt;
&lt;td&gt;What is the workflow's current status?&lt;/td&gt;
&lt;td&gt;Guardedly mutable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit history&lt;/td&gt;
&lt;td&gt;What changed, when, and by which actor/system?&lt;/td&gt;
&lt;td&gt;Append-only or tamper-evident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Financial ledger&lt;/td&gt;
&lt;td&gt;Which monetary obligations or movements have been recorded?&lt;/td&gt;
&lt;td&gt;Append-only journal with corrections as new entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider settlement report&lt;/td&gt;
&lt;td&gt;What did the provider actually settle, deduct, or transfer?&lt;/td&gt;
&lt;td&gt;External reconciliation evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A financial ledger commonly uses &lt;strong&gt;double-entry bookkeeping&lt;/strong&gt;: each journal transaction contains balanced postings across accounts under the chosen accounting model. A ledger entry is not automatically proof that funds have settled in the bank account.&lt;/p&gt;

&lt;p&gt;For example, an authorized card payment is not the same as a captured payment; a captured payment is not necessarily already settled to the merchant. Provider fees, reserves, disputes, reversals, and payout timing may affect reconciliation.&lt;/p&gt;

&lt;p&gt;Avoid saying that a &lt;code&gt;payment.succeeded&lt;/code&gt; event means &lt;em&gt;cash is already in the merchant's bank account&lt;/em&gt;. What it means depends on the provider event and payment method.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Authorization, capture, void, and refund
&lt;/h2&gt;

&lt;p&gt;Not every payment should be charged immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immediate charge/capture&lt;/strong&gt; can suit purchases where fulfillment and amount are known upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorize then capture&lt;/strong&gt; can suit hotels, rentals, or orders where inventory or final amount needs confirmation. An authorization generally reserves or approves funds subject to network/provider rules; capture is a later step requesting the charge. Authorizations can expire, and partial/multiple captures are provider- and payment-method-dependent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Void/cancel&lt;/strong&gt; typically applies before capture or while an operation remains cancellable. &lt;strong&gt;Refund&lt;/strong&gt; is a separate operation after a charge/capture, subject to provider rules and payment state.&lt;/p&gt;

&lt;p&gt;Refunds need their own IDs, attempts, idempotency identities, statuses, and reconciliation. A refund timeout is ambiguous for the same reason a charge timeout is ambiguous.&lt;/p&gt;

&lt;p&gt;For partial refunds, protect the available refundable amount against concurrent requests. A safe local reservation/transaction must account for &lt;strong&gt;successful and still-in-flight refund amounts&lt;/strong&gt;, not merely the sum of completed refunds, otherwise two concurrent provider calls can each appear individually valid and together exceed the captured amount. Provider-side refund limits remain an additional protection, not a substitute for local coordination.&lt;/p&gt;

&lt;p&gt;Refund ledger postings should be new entries, not edits that erase the original charge history.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Reconciliation is a core subsystem, not an emergency script
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fur1v8gx3p6f7edb1ee87.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fur1v8gx3p6f7edb1ee87.png" alt="reconciliation" width="799" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Webhooks are not guaranteed to arrive promptly or exactly once. Provider APIs and internal databases can temporarily disagree. Reconciliation detects and resolves the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational reconciliation&lt;/strong&gt; checks in-flight, stale, and unresolved operations against provider APIs or other supported status channels. It can identify a charge that succeeded externally but never updated the internal state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Financial reconciliation&lt;/strong&gt; compares internal financial records against provider reports, settlement exports, fees, payouts, and adjustments. It can reveal mismatched amounts, missing transactions, unexpected duplicates, refunds, and reversals that payment-status polling alone would not catch.&lt;/p&gt;

&lt;p&gt;A production reconciliation job should have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stable provider and internal reference mappings.&lt;/li&gt;
&lt;li&gt;Bounded, restartable batches with checkpoints or watermarks.&lt;/li&gt;
&lt;li&gt;Rate-limit-aware status lookups.&lt;/li&gt;
&lt;li&gt;Idempotent repair operations with durable action IDs.&lt;/li&gt;
&lt;li&gt;Exception queues for unresolved mismatches requiring investigation.&lt;/li&gt;
&lt;li&gt;Alerts for aging unresolved payments, ledger gaps, and settlement differences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not automatically trust whichever record has the newest timestamp. Interpret the provider's event semantics and the settlement evidence, then record corrections without silently rewriting financial history.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Multi-provider routing has a double-charge trap
&lt;/h2&gt;

&lt;p&gt;A second provider can improve availability, but failover is not simply retrying elsewhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider A: request timed out, but charge actually succeeded
Provider B: automatic failover charge also succeeds
Result: customer may be charged twice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different providers generally cannot deduplicate each other's charges using a shared idempotency key.&lt;/p&gt;

&lt;p&gt;Only route a new independent attempt when the previous attempt is known not to have created a charge, or when a specifically designed orchestration and recovery protocol makes that decision safe. Keep each provider attempt separately identifiable under the same logical payment.&lt;/p&gt;

&lt;p&gt;Circuit breakers, rate limits, exponential backoff, and jitter protect capacity, but &lt;strong&gt;none of them resolves an already ambiguous financial outcome&lt;/strong&gt;. A &lt;code&gt;5xx&lt;/code&gt; is not universally proof that a provider performed no side effect; follow that provider's error semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Security and compliance boundaries
&lt;/h2&gt;

&lt;p&gt;For most integrations, prefer provider-hosted payment collection or tokenization so your application avoids directly handling raw card details wherever possible.&lt;/p&gt;

&lt;p&gt;Tokenization can reduce PCI DSS scope, but does &lt;strong&gt;not&lt;/strong&gt; automatically remove all compliance obligations. Exact scope depends on how the integration works, what systems can affect the payment page, and the applicable standards and assessments.&lt;/p&gt;

&lt;p&gt;Keep payment API authorization strict: a user must not retrieve or refund another merchant's payment by guessing an identifier. Verify webhook authenticity, prevent replay where the provider supports it, protect credentials, limit sensitive data in logs, and audit privileged refund and payment-data access.&lt;/p&gt;

&lt;p&gt;Never store card verification values after authorization; any handling of cardholder data must follow the applicable payment security rules. Payment references, personal information, and webhook payloads can also be sensitive even when they are not raw card numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Scaling without weakening correctness
&lt;/h2&gt;

&lt;p&gt;Payment APIs can be horizontally scaled because authoritative state belongs in durable systems, not process memory. The harder bottlenecks tend to be database contention, provider quotas, webhook throughput, and reconciliation backlog.&lt;/p&gt;

&lt;p&gt;A pragmatic progression:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with a strongly consistent transactional store for payment identity, idempotency, attempts, and guarded state changes.&lt;/li&gt;
&lt;li&gt;Add appropriate indexes for merchant/payment lookup, unresolved attempts, provider references, and reconciliation scheduling.&lt;/li&gt;
&lt;li&gt;Scale webhook processing, outbox relay, and downstream workers independently with bounded concurrency and backpressure.&lt;/li&gt;
&lt;li&gt;Use replicas for reporting and stale-tolerant reads, not for authoritative decisions such as whether a charge already exists.&lt;/li&gt;
&lt;li&gt;Partition or shard only when measured bottlenecks justify the complexity; preserve uniqueness and ownership invariants across partitions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Multi-region payment writes require especially careful ownership and fencing. If two regions can simultaneously accept the same idempotency key without a shared uniqueness guarantee, each may initiate a separate provider charge. A failover design must also preserve or recover in-flight attempt identities, not just database rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational metrics&lt;/strong&gt; should include provider timeouts by operation, ambiguous-attempt age, idempotency conflicts, webhook verification failures and processing lag, outbox age, refund reservation backlog, reconciliation mismatches, and ledger/settlement discrepancies. Ordinary request latency alone cannot tell you whether a payment system is financially correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Walk through the failures that matter
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Unsafe reaction&lt;/th&gt;
&lt;th&gt;Safer design&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client retries after timeout&lt;/td&gt;
&lt;td&gt;Create a new charge&lt;/td&gt;
&lt;td&gt;Reuse logical payment and provider attempt identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two concurrent payment requests&lt;/td&gt;
&lt;td&gt;Both pass a cache check&lt;/td&gt;
&lt;td&gt;Atomic durable idempotency constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider response is lost&lt;/td&gt;
&lt;td&gt;Mark payment failed&lt;/td&gt;
&lt;td&gt;Preserve unresolved state; query, safely retry, or reconcile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook delivered twice&lt;/td&gt;
&lt;td&gt;Reapply financial effects&lt;/td&gt;
&lt;td&gt;Durable event/business-operation deduplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Older webhook arrives late&lt;/td&gt;
&lt;td&gt;Overwrite latest state&lt;/td&gt;
&lt;td&gt;Guarded transitions and provider semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB commits but event publish fails&lt;/td&gt;
&lt;td&gt;Lose downstream confirmation&lt;/td&gt;
&lt;td&gt;Transactional outbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund requests race&lt;/td&gt;
&lt;td&gt;Each sees old refundable total&lt;/td&gt;
&lt;td&gt;Transactionally reserve refundable amount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider A times out&lt;/td&gt;
&lt;td&gt;Immediately charge at Provider B&lt;/td&gt;
&lt;td&gt;Resolve A before independent failover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment recorded but ledger posting missing&lt;/td&gt;
&lt;td&gt;Assume reporting will fix itself&lt;/td&gt;
&lt;td&gt;Durable delivery, idempotent posting, monitored reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary region fails&lt;/td&gt;
&lt;td&gt;Accept writes everywhere&lt;/td&gt;
&lt;td&gt;Explicit write ownership, fencing, recovery of ambiguous attempts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a fuller production failure analysis, see &lt;a href="https://seeitflow.com/system-design/intermediate/payment/failure-scale-scenarios" rel="noopener noreferrer"&gt;Payment System — Failure &amp;amp; Scale Scenarios&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. The final architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  Customer / Checkout
                           |
                           v
                     Payment API
                           |
                           v
                 Payment Orchestrator
                    /      |       \
                   /       |        \
          Payment DB   Provider      Status API
          + Attempts   Adapter           |
          + Idempotency   |              |
          + Outbox        v              |
          + Ledger*    External PSP &amp;lt;----+
                |          |
                |          v
                |     Webhook Endpoint
                |          |
                |     Verified Durable Inbox
                |          |
                |     Idempotent Worker
                |          |
                +&amp;lt;---------+
                |
             Outbox Relay ---&amp;gt; Broker ---&amp;gt; Order / Notification
                |
          Reconciliation Jobs &amp;lt;---- Provider API / Reports

* Ledger may be in the same transactional boundary or a
  separately operated financial subsystem, with explicit
  consistency and reconciliation guarantees.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frj1s4e7k1sfcel4p5y90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frj1s4e7k1sfcel4p5y90.png" alt="final architecture" width="799" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every component exists for a reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payment orchestrator:&lt;/strong&gt; one logical payment and guarded state transitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable idempotency:&lt;/strong&gt; duplicate client requests cannot create independent logical payments within the key's contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider adapter:&lt;/strong&gt; normalizes provider-specific operations and outcome semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook inbox:&lt;/strong&gt; authenticated, durable, duplicate-tolerant asynchronous updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbox:&lt;/strong&gt; durable handoff to downstream business processes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ledger:&lt;/strong&gt; auditable financial postings, distinct from workflow status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation:&lt;/strong&gt; finds and resolves external/internal discrepancies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is not meant to imply that all components must be separate microservices. Several can begin as modules within one well-structured service and database.&lt;/p&gt;

&lt;h2&gt;
  
  
  17. How to explain this in a system design interview
&lt;/h2&gt;

&lt;p&gt;A strong payment-system answer can follow this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clarify the payment method, synchronous versus asynchronous outcomes, authorization/capture, refunds, and provider count.&lt;/li&gt;
&lt;li&gt;Define logical payment identity, attempt identity, money representation, and the state machine.&lt;/li&gt;
&lt;li&gt;Walk through the happy path, persisting the attempt before the provider call.&lt;/li&gt;
&lt;li&gt;Demonstrate duplicate-request handling using durable idempotency.&lt;/li&gt;
&lt;li&gt;Introduce timeout ambiguity, safe provider retries, webhooks, and reconciliation.&lt;/li&gt;
&lt;li&gt;Close the database-to-queue gap with an outbox and idempotent consumers.&lt;/li&gt;
&lt;li&gt;Separate operational payment state from financial ledger and settlement.&lt;/li&gt;
&lt;li&gt;Discuss refunds, multi-provider failover, security, and scaling only to the depth the interview requires.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The strongest answers do not promise “exactly-once payments” without qualifications. They explain &lt;strong&gt;which boundary enforces which invariant, where external uncertainty remains, and how discrepancies are detected and corrected&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Explore the deeper decisions in &lt;a href="https://seeitflow.com/system-design/intermediate/payment/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt;, compare options in &lt;a href="https://seeitflow.com/system-design/intermediate/payment/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions&lt;/a&gt;, and practice the follow-up questions in &lt;a href="https://seeitflow.com/system-design/intermediate/payment/interview-perspective" rel="noopener noreferrer"&gt;Interview Perspective&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A payment system is not correct merely because its happy path succeeds. It is correct to the extent that it can preserve durable identity, avoid duplicate external effects, represent uncertainty honestly, recover from crashes, and reconcile what it believes happened against what the provider and financial records show.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question is not only “Did the API succeed?” It is “What financial effect actually occurred, how do we know, and what happens when we do not know yet?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the complete visual and technical learning path, explore &lt;a href="https://seeitflow.com/system-design/intermediate/payment/complete-design" rel="noopener noreferrer"&gt;Payment System Design on SeeItFlow&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>backend</category>
      <category>payments</category>
    </item>
    <item>
      <title>Distributed Task Scheduler System Design: Jobs, Retries, Leases, and Failures</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Tue, 13 Oct 2026 05:09:43 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/distributed-task-scheduler-system-design-jobs-retries-leases-and-failures-3gef</link>
      <guid>https://dev.to/mangeshmandlik/distributed-task-scheduler-system-design-jobs-retries-leases-and-failures-3gef</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bwqv2yun7ayhtauybcw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bwqv2yun7ayhtauybcw.png" alt="distributed task scheduler final architecture" width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Distributed Task Scheduler System Design: What Happens When a Job Is Due?
&lt;/h2&gt;

&lt;p&gt;A user schedules a payment for 9:00 AM. At 9:00, the scheduler finds the job and hands it to a worker. The worker calls the payment provider, the charge succeeds, and then the worker crashes before recording success.&lt;/p&gt;

&lt;p&gt;What should happen next?&lt;/p&gt;

&lt;p&gt;The scheduler cannot simply forget the job: it has no durable confirmation that execution finished. But blindly running it again might charge the customer twice.&lt;/p&gt;

&lt;p&gt;That tension is central to &lt;strong&gt;distributed task scheduler system design&lt;/strong&gt;. Finding work at the right time is only the beginning. A production scheduler must preserve jobs across failures, coordinate concurrent schedulers, handle retries without inventing exactly-once guarantees, and make recovery safe even when the outcome of an earlier attempt is unknown.&lt;/p&gt;

&lt;p&gt;This article designs a &lt;strong&gt;distributed job scheduler&lt;/strong&gt; for one-time and recurring tasks, with the focus on the correctness boundaries that matter in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, define what the scheduler promises
&lt;/h2&gt;

&lt;p&gt;A task scheduler answers &lt;strong&gt;when work becomes eligible to run&lt;/strong&gt;. It does not inherently guarantee that a worker starts at an exact wall-clock instant, that a remote API succeeds, or that an external side effect happens exactly once.&lt;/p&gt;

&lt;p&gt;A useful contract separates four timestamps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;due_at&lt;/code&gt;: the earliest instant a job is eligible to run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dispatched_at&lt;/code&gt;: when the system hands an execution request to its delivery mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;started_at&lt;/code&gt;: when a worker begins the attempt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;completed_at&lt;/code&gt;: when the system durably records a terminal outcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 9:00 AM job may begin after 9:00 because of polling, queueing, worker capacity, or an outage. The product should define an acceptable &lt;strong&gt;scheduling lag&lt;/strong&gt; and whether a late job should still execute. For workloads with hard deadlines, expiry is a separate policy: running a stale trade or promotion after its validity window may be worse than skipping it.&lt;/p&gt;

&lt;p&gt;We'll assume jobs can be scheduled once or on a recurrence, cancelled or rescheduled before execution, retried for eligible failures, and queried for status. We will not assume every job is safe to repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client / API
     |
     v
Durable Job Database  &amp;lt;----- Recurrence Generator
     |                       (creates occurrences)
     v
Due-Job Discovery / Claimers
     |
     | atomic DB transaction:
     | claim + dispatch-outbox record
     v
Transactional Outbox
     |
     v
Outbox Relay -----&amp;gt; Durable Queue -----&amp;gt; Workers
                                           |
                                           v
                                    Business Side Effect
                                           |
                                           v
                                  Status / Attempt Store

Lease Reaper ------&amp;gt; expired attempts / safe retry decisions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database owns the authoritative job state. The queue transports work to workers; it does not replace the scheduler's lifecycle record in this design. The outbox bridges the database-to-queue failure gap. A lease reaper finds attempts that stopped making progress.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;one reasonable architecture&lt;/strong&gt;, not a mandatory stack. A smaller scheduler can begin with indexed database polling and workers claiming rows directly. Some workloads use broker-native delayed delivery; others add Redis sorted sets or timing wheels for due-time discovery. Each alternative has different limits and failure semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Store jobs durably before promising acceptance
&lt;/h2&gt;

&lt;p&gt;A minimal model might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jobs
  job_id
  tenant_id
  payload_ref
  due_at
  status
  version
  creation_idempotency_key
  deadline_at

attempts
  attempt_id
  job_id
  generation
  status
  lease_expires_at
  started_at
  completed_at
  error_category

outbox
  event_id
  attempt_id
  payload
  published_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For recurring work, add a &lt;strong&gt;schedule definition&lt;/strong&gt; containing the recurrence rule, IANA timezone, misfire policy, overlap policy, and the next occurrence to generate. Each generated occurrence gets its own job identity.&lt;/p&gt;

&lt;p&gt;The creation API should make a durable write before returning an unqualified success response. If a client times out after the commit, a stable creation idempotency key and a uniqueness constraint let it retry without creating a second logical job.&lt;/p&gt;

&lt;p&gt;Job status is not the same as an attempt's status. A job may have multiple attempts; one attempt can fail transiently while the logical job remains eligible for retry. Keep enough attempt history to explain what happened without treating an old attempt's completion as permission to overwrite newer state.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Discover due jobs without scanning everything
&lt;/h2&gt;

&lt;p&gt;The simplest viable due-work detector is a database index supporting a query shaped like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SCHEDULED'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;due_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;due_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relevant index, bounded batch size, and query plan matter more than the particular batch size shown here. In a multi-scheduler design, merely selecting rows is not sufficient: two instances could read the same jobs. A claim must be &lt;strong&gt;atomic&lt;/strong&gt; and conditional on current state, commonly using row locks such as &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; inside a transaction or an equivalent compare-and-swap transition.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SKIP LOCKED&lt;/code&gt; is a useful mechanism, not a universal requirement. Another design gives one elected leader ownership of due-time discovery. At higher scale, ownership can be sharded so independent schedulers process different partitions. Leader-based systems need fencing: a leader whose lease expired may resume after a pause and must not be allowed to make stale authoritative writes.&lt;/p&gt;

&lt;p&gt;A Redis sorted set can speed up discovery when database due-time queries become a measured bottleneck. But if Redis is only an index, its loss must not erase jobs: rebuild it from the durable store, validate candidate jobs against authoritative state, and reconcile missing entries. A timing wheel similarly needs a durable backstop if its in-memory state can disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The database-to-queue dual-write trap
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffef8agfgajnatad8z3di.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffef8agfgajnatad8z3di.png" alt="durable dispatch path" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Consider this dispatch sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update a job to &lt;code&gt;QUEUED&lt;/code&gt; in the database.&lt;/li&gt;
&lt;li&gt;Publish an execution message to Kafka, SQS, or another broker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the process crashes between steps, the database says the job was dispatched, but the broker never received it. Reversing the order creates the mirror failure: the broker receives a message, but the database update fails, so a retry may publish it again.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;transactional outbox&lt;/strong&gt; handles the first problem without pretending the database and broker share one atomic transaction. In one database transaction, the scheduler records the job/attempt transition &lt;strong&gt;and&lt;/strong&gt; inserts an outbox event. A relay later publishes that event to the broker and records publication progress.&lt;/p&gt;

&lt;p&gt;The relay itself can crash after publishing but before marking the event published. It may then publish the same event again. Thus the outbox supports &lt;strong&gt;eventual, at-least-once publication&lt;/strong&gt;, assuming the relay and broker recover and retry successfully; it does &lt;strong&gt;not&lt;/strong&gt; make downstream execution exactly once.&lt;/p&gt;

&lt;p&gt;Messages need stable identities. Repeated delivery of the same dispatch event should not create an unrelated business operation. The worker checks authoritative job and attempt state before claiming execution, and downstream effects still require their own idempotency protections.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Worker leases handle crashes, not exactly-once effects
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwlq3xz6xoj3il1dtoifv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwlq3xz6xoj3il1dtoifv.png" alt="worker lease protocol" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A worker claims an attempt with a lease. While running, it renews the lease if the design supports long-running work. If the worker dies, a separate reaper can detect the expired lease and make the job eligible for another attempt.&lt;/p&gt;

&lt;p&gt;But lease expiry is &lt;strong&gt;not proof that the old worker stopped&lt;/strong&gt;. A long garbage-collection pause, lost heartbeat, or network partition can cause a live worker to lose its lease while it continues processing. Another worker may start at the same time.&lt;/p&gt;

&lt;p&gt;To protect scheduler state, attach an increasing attempt generation or fencing token to conditional writes. A stale worker must not be able to mark a superseded attempt successful or overwrite the current state. Fencing protects only resources that actually validate it; it cannot retroactively undo a payment sent to an external provider.&lt;/p&gt;

&lt;p&gt;The lease duration and heartbeat cadence must be chosen against real pause times, execution durations, and acceptable recovery latency. There is no universally correct TTL or heartbeat ratio.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Retry correctness depends on where the side effect happens
&lt;/h2&gt;

&lt;p&gt;Return to the payment example. Worker A successfully charges the customer but crashes before saving the result. Worker B eventually receives a retry. From the scheduler's local records, the outcome is ambiguous.&lt;/p&gt;

&lt;p&gt;A database flag saying &lt;strong&gt;started&lt;/strong&gt; is not enough. If a worker writes that flag and crashes &lt;em&gt;before&lt;/em&gt; the charge, treating the flag as proof of completion silently loses the payment. A completion marker written before an external effect has the same flaw.&lt;/p&gt;

&lt;p&gt;There are three importantly different cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local database mutation.&lt;/strong&gt; If the business change and completion record live in the same database, commit them in one transaction, with appropriate uniqueness or conditional constraints. Either both commit or neither does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External API supporting idempotency.&lt;/strong&gt; Send a stable business-operation idempotency key that survives retries and worker replacement. The provider's documented deduplication semantics and retention window determine what guarantee this actually gives. An internal attempt ID is usually the wrong key because it changes across attempts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External API without reliable idempotency.&lt;/strong&gt; A timeout cannot reveal whether the operation happened. Durable pending state, provider-side lookup where available, and reconciliation may resolve uncertainty. If the provider offers neither deduplication nor reliable outcome lookup, the scheduler cannot promise exactly-once external effects. The product must explicitly choose how to handle that residual risk.&lt;/p&gt;

&lt;p&gt;This is why &lt;strong&gt;at-least-once execution attempts&lt;/strong&gt; and &lt;strong&gt;exactly-once business effects&lt;/strong&gt; are not interchangeable claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Retry policy is more than exponential backoff
&lt;/h2&gt;

&lt;p&gt;Retries begin with &lt;strong&gt;failure classification&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transient failures, such as some timeouts or temporary service unavailability, may be retried if the operation is safe to retry.&lt;/li&gt;
&lt;li&gt;Rate-limit responses require respecting the provider's applicable retry guidance and quotas; an HTTP 429 is not permission to retry immediately.&lt;/li&gt;
&lt;li&gt;Permanent validation or business-rule failures generally should not be retried unchanged.&lt;/li&gt;
&lt;li&gt;Ambiguous outcomes require idempotency or reconciliation, not optimistic assumptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use bounded attempts, capped exponential backoff with jitter, execution deadlines, and a dead-letter or terminal-failure path. Backoff spreads attempts over time, but does not by itself cap aggregate load on a struggling dependency. A dependency-scoped rate limit or retry budget, and circuit breaking when appropriate, prevent thousands of independently reasonable retries from combining into a retry storm.&lt;/p&gt;

&lt;p&gt;A dead-letter queue is not a garbage bin. Preserve job identity, failure reason, and attempt history. Replays should be authorized, observable, rate-limited, and idempotent. A replay may create a new attempt, but it must not invent a new business operation identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Recurring schedules need separate occurrence identities
&lt;/h2&gt;

&lt;p&gt;“Every day at 9:00 AM” is a &lt;strong&gt;schedule definition&lt;/strong&gt;, not one job repeatedly moved back to &lt;code&gt;SCHEDULED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Generate individual occurrences, each with a deterministic identity based on the schedule and nominal occurrence. A unique constraint ensures two generators recovering from a failover do not create duplicate logical occurrences.&lt;/p&gt;

&lt;p&gt;For a recurring schedule, store the recurrence rule &lt;strong&gt;and an IANA timezone&lt;/strong&gt;, such as &lt;code&gt;Asia/Kolkata&lt;/code&gt; or &lt;code&gt;America/New_York&lt;/code&gt;. A fixed UTC offset is not sufficient for local-time recurrences across daylight-saving changes. Every occurrence must be resolved using the timezone rules applicable to that date.&lt;/p&gt;

&lt;p&gt;Two edge cases need explicit product decisions: during spring-forward, a configured local time might not exist; during fall-back, it might occur twice. Decide whether to skip, shift, or select a particular occurrence rather than relying silently on a date library's default.&lt;/p&gt;

&lt;p&gt;After downtime, decide whether to &lt;strong&gt;skip missed runs, catch up all valid runs, or coalesce missed runs&lt;/strong&gt;. For billing, every period might matter; for a transient reminder, replaying all missed notifications could be actively harmful. Also specify whether successive occurrences may overlap, should queue behind one another, or should skip while an earlier occurrence is still running.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Cancellation is a race, not just a status update
&lt;/h2&gt;

&lt;p&gt;Cancelling a scheduled job before it is claimed is relatively straightforward. Cancelling a queued job is harder because its message may already be in transit. A worker should check authoritative state and the dispatched version before beginning work; a stale message must not bypass a recorded cancellation.&lt;/p&gt;

&lt;p&gt;That check still does not make cancellation of &lt;strong&gt;running external work&lt;/strong&gt; instantaneous or reversible. Cancellation may be cooperative, and there can be a race after the final pre-execution check. A precise API distinguishes &lt;strong&gt;cancellation requested&lt;/strong&gt;, &lt;strong&gt;cancelled before execution&lt;/strong&gt;, and &lt;strong&gt;already running or completed&lt;/strong&gt;. Never promise that a successfully returned cancellation can undo an email already sent or a charge already made.&lt;/p&gt;

&lt;p&gt;Rescheduling also changes job version and due time; old queued messages must not cause execution against an obsolete schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. What happens at the 9:00 AM burst?
&lt;/h2&gt;

&lt;p&gt;Imagine millions of jobs become due at the same moment. It is tempting to prescribe Redis or a bigger queue immediately. Neither is a universal answer.&lt;/p&gt;

&lt;p&gt;The first bottleneck might be the due-time index, claim contention, outbox relay, broker ingress, worker concurrency, network egress, or a third-party API's quota. Measure &lt;strong&gt;due-to-dispatch lag&lt;/strong&gt;, &lt;strong&gt;queue waiting time&lt;/strong&gt;, &lt;strong&gt;worker start lag&lt;/strong&gt;, &lt;strong&gt;oldest overdue job age&lt;/strong&gt;, &lt;strong&gt;retry rate&lt;/strong&gt;, and &lt;strong&gt;downstream throttling&lt;/strong&gt; to identify where capacity is actually exhausted.&lt;/p&gt;

&lt;p&gt;Bound due-job scans, partition hot scheduling ranges when needed, enforce per-tenant quotas and fairness, and pace dispatch to sustainable downstream throughput. Predictable bursts may justify pre-scaling workers.&lt;/p&gt;

&lt;p&gt;Pre-staging work before its due time can help only if execution is still gated by &lt;code&gt;eligible_at&lt;/code&gt;. Starting a 9:00 AM payment at 8:59 merely to flatten the graph is a correctness regression unless early execution is explicitly allowed.&lt;/p&gt;

&lt;p&gt;A backlog is not necessarily data loss. But a scheduler that keeps accepting jobs faster than they can execute needs an explicit lateness and admission-control policy; a queue cannot manufacture downstream capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Recovery paths worth designing before production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scheduler crashes.&lt;/strong&gt; Durable due jobs remain discoverable. Another claimer or a fenced new leader takes over, then performs a bounded catch-up scan rather than dispatching every overdue job at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database primary fails.&lt;/strong&gt; Job creation and authoritative transitions may become unavailable. Reject uncommitted new requests explicitly instead of pretending an in-memory buffer is durable. Replica promotion's data-loss exposure depends on replication acknowledgments and actual lag, not a universal recovery number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broker is unavailable.&lt;/strong&gt; Outbox events remain durable and publication lag rises. Once the broker returns, drain the outbox at a controlled rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worker crashes or an ACK is lost.&lt;/strong&gt; A lease expiry or broker redelivery may cause another attempt. That is expected at-least-once behavior; side-effect idempotency or reconciliation is the safety boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A leader resumes after losing ownership.&lt;/strong&gt; Conditional state transitions and fencing reject stale authoritative writes. Merely holding a time-limited lock is insufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A region fails.&lt;/strong&gt; A standby region can take over only under a defined ownership and fencing protocol, using whatever state was durably replicated. Recovery time and potential data loss are properties of the chosen replication and failover design, not free guarantees of “multi-region.”&lt;/p&gt;

&lt;h2&gt;
  
  
  11. The trade-offs that change the design
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Simpler starting point&lt;/th&gt;
&lt;th&gt;When a different approach earns its cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Due-time discovery&lt;/td&gt;
&lt;td&gt;Indexed database polling&lt;/td&gt;
&lt;td&gt;Redis index, timing wheel, or sharding when measured due-discovery limits justify them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler coordination&lt;/td&gt;
&lt;td&gt;Atomic competing DB claimers&lt;/td&gt;
&lt;td&gt;Leader election or shard ownership for particular contention and operational requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dispatch&lt;/td&gt;
&lt;td&gt;Direct database-backed worker claims for a small design&lt;/td&gt;
&lt;td&gt;Queue + transactional outbox for decoupled, durable, high-throughput handoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker recovery&lt;/td&gt;
&lt;td&gt;Fixed leases for bounded short jobs&lt;/td&gt;
&lt;td&gt;Renewable leases and checkpoints for long or variable-duration jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missed recurrences&lt;/td&gt;
&lt;td&gt;Explicit per-schedule misfire policy&lt;/td&gt;
&lt;td&gt;Different catch-up behavior by workload, not a universal replay-all rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regional topology&lt;/td&gt;
&lt;td&gt;One authoritative region with standby&lt;/td&gt;
&lt;td&gt;More complex cross-region ownership only when availability needs justify it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The goal is not to collect the most distributed components. It is to add mechanisms that solve a demonstrated bottleneck or close a specific correctness gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical way to reason through the interview
&lt;/h2&gt;

&lt;p&gt;Start by asking what “run at 9:00” means, how late jobs may be, whether retries and overlap are acceptable, and which external side effects cannot safely repeat. Establish peak due-job rate and burstiness as &lt;strong&gt;assumptions&lt;/strong&gt;, not universal scale thresholds.&lt;/p&gt;

&lt;p&gt;Then evolve the design in order: durable jobs; indexed due discovery and atomic claiming; a dispatch boundary; worker leases; retry classification; idempotency at the business-effect boundary; and failure recovery. Add an outbox when dispatch crosses a database/broker boundary. Add a fast due-time index or sharded ownership only when its benefits outweigh the consistency and operational costs.&lt;/p&gt;

&lt;p&gt;A strong system design explanation follows one job through an actual failure. The most revealing question is often not “how do you schedule a million jobs?” but &lt;strong&gt;“what do you do when the external effect succeeded and your worker cannot prove it?”&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The essential takeaway
&lt;/h2&gt;

&lt;p&gt;A distributed task scheduler is a &lt;strong&gt;durable coordination system under uncertain execution outcomes&lt;/strong&gt;. Its clock determines eligibility, its store preserves intent, its dispatcher delivers work, and its workers perform effects. Those boundaries can fail independently.&lt;/p&gt;

&lt;p&gt;Reliable scheduling therefore comes from a combination of atomic claims, durable dispatch, leases and fencing, bounded retries, explicit recurrence semantics, and idempotency or reconciliation at the point where the business effect actually occurs. None of these alone provides a universal exactly-once guarantee.&lt;/p&gt;

&lt;p&gt;For the full visual and production-oriented treatment, explore &lt;a href="https://seeitflow.com/system-design/foundations/task-scheduler/complete-design" rel="noopener noreferrer"&gt;Distributed Task Scheduler — Complete Design&lt;/a&gt;, &lt;a href="https://seeitflow.com/system-design/foundations/task-scheduler/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt;, &lt;a href="https://seeitflow.com/system-design/foundations/task-scheduler/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions&lt;/a&gt;, and &lt;a href="https://seeitflow.com/system-design/foundations/task-scheduler/failure-scale-scenarios" rel="noopener noreferrer"&gt;Failure &amp;amp; Scale Scenarios&lt;/a&gt; on &lt;strong&gt;SeeItFlow&lt;/strong&gt;. The &lt;a href="https://seeitflow.com/system-design/foundations/task-scheduler/visual-design-journey" rel="noopener noreferrer"&gt;Visual Design Journey&lt;/a&gt; walks through the design's evolution interactively.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributed</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>WhatsApp System Design: Building a Reliable Messaging Platform</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:52:31 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/whatsapp-system-design-building-a-reliable-messaging-platform-483e</link>
      <guid>https://dev.to/mangeshmandlik/whatsapp-system-design-building-a-reliable-messaging-platform-483e</guid>
      <description>&lt;p&gt;&lt;em&gt;A chat message is easy to send when both users are online. The difficult part is ensuring that one logical message survives retries, reaches multiple devices, appears in a consistent order, and remains recoverable when connections or services fail.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftvchrwp7sgiubi3ot229.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftvchrwp7sgiubi3ot229.png" alt="final architecture" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagine Alice sends Bob a message. Bob's laptop is connected, but his phone is offline. The server accepts and durably stores the message, then its acknowledgement to Alice is lost. Alice's app cannot tell whether the send succeeded, so it retries. Meanwhile, Bob's laptop receives the message, and his phone will reconnect hours later.&lt;/p&gt;

&lt;p&gt;What should happen? Alice must not create a second logical message by retrying. Bob's laptop should not display duplicates. Bob's phone should eventually catch up, subject to the system's retention and access policies. And the sender's &lt;strong&gt;sent&lt;/strong&gt;, &lt;strong&gt;delivered&lt;/strong&gt;, and &lt;strong&gt;read&lt;/strong&gt; indicators must not pretend to mean the same thing.&lt;/p&gt;

&lt;p&gt;That is the real challenge behind designing a WhatsApp-like messaging platform. This article develops an illustrative architecture for a real-time chat system; it does &lt;strong&gt;not&lt;/strong&gt; claim to describe WhatsApp's private implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements before architecture
&lt;/h2&gt;

&lt;p&gt;Start with one-to-one messages, group conversations, low-latency delivery to connected clients, offline recovery, multi-device synchronization, delivery and read receipts, and media attachments. Presence and typing indicators improve the experience, but they are not part of the durable-message guarantee. Broadcast channels, message editing, and end-to-end encryption introduce additional design constraints that we will address separately.&lt;/p&gt;

&lt;p&gt;The critical non-functional requirements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Durability:&lt;/strong&gt; an acknowledged send has satisfied a clearly defined durable-write contract, even if the recipient is offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency:&lt;/strong&gt; retrying the same logical send does not create another accepted message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordering:&lt;/strong&gt; messages have a consistent order &lt;em&gt;within a conversation&lt;/em&gt;, without requiring one global order for the entire platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery:&lt;/strong&gt; disconnected devices can reconcile missed changes from durable state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; socket connections, message writes, history reads, group delivery, and media bytes can scale independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; authenticate devices, authorize conversation operations, and define encryption and retention policies explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We should not promise unconditional instant delivery, exactly-once network transmission, or zero data loss under every possible disaster. The guarantees depend on the storage write policy, failure model, retention window, and client behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  A scale model, not a claim about WhatsApp
&lt;/h3&gt;

&lt;p&gt;For a sizing exercise, assume 500 million daily active users, 200 million concurrent connections, and 100 billion messages per day. That is approximately &lt;strong&gt;1.16 million messages per second on average&lt;/strong&gt;. If the effective stored message record averages 1 KB, the raw logical ingest is about &lt;strong&gt;100 TB/day&lt;/strong&gt;, or &lt;strong&gt;3 PB over 30 days&lt;/strong&gt;, before replication, indexes, attachments, and other overhead. Peaks may be substantially higher.&lt;/p&gt;

&lt;p&gt;These are illustrative assumptions, not published operating figures. They tell us to plan separately for persistent sockets, sustained writes, fan-out amplification, reconnect bursts, and storage growth. A hypothetical 50,000 sockets per gateway would imply about 4,000 gateways for 200 million connections &lt;em&gt;before&lt;/em&gt; spare capacity; actual density depends on workload, memory, TLS, runtime, network throughput, and failure headroom.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Start simple, then find the breaking point
&lt;/h2&gt;

&lt;p&gt;A small chat application can start with clients connecting to an application server backed by a relational database. The server authenticates requests, writes messages, and forwards them to connected recipients.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhzc8ig6boxkm4xh1c5wk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhzc8ig6boxkm4xh1c5wk.png" alt="start simple" width="800" height="320"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice ──&amp;gt; App Server ──&amp;gt; Message Database
                │
                └──────&amp;gt; Bob (if connected)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works at modest scale. But once users are connected to many application instances, Alice's server cannot assume Bob is connected to the same one. Long-lived sockets consume a different kind of capacity from database writes. Offline delivery and group messaging also need durable recovery rather than in-memory queues.&lt;/p&gt;

&lt;p&gt;The next architecture separates &lt;strong&gt;connection management&lt;/strong&gt;, &lt;strong&gt;message acceptance and storage&lt;/strong&gt;, and &lt;strong&gt;delivery/synchronization&lt;/strong&gt;. These boundaries solve different problems; they are not microservices added merely for appearance.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. WebSocket gateways and connection routing
&lt;/h2&gt;

&lt;p&gt;WebSocket is a practical default for interactive chat because it supports persistent bidirectional communication. The client can send messages and acknowledgements while the server pushes new events over the same connection.&lt;/p&gt;

&lt;p&gt;Server-Sent Events plus HTTP requests can work for primarily server-to-client applications; long polling is another compatible fallback. Neither is inherently invalid, but frequent bidirectional chat tends to favor WebSocket. HTTP/2 Server Push is not the relevant modern alternative.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice's phone ──&amp;gt; Load Balancer ──&amp;gt; Gateway A ──┐
                                                │
Bob's laptop ──&amp;gt; Load Balancer ──&amp;gt; Gateway B ────┼──&amp;gt; Messaging Service
                                                │           │
Bob's phone (offline)                           │           v
                                                │      Message Store
                  Connection Registry &amp;lt;────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A gateway authenticates the device session, maintains heartbeats and bounded socket buffers, and registers its route. The connection registry conceptually maps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user_id -&amp;gt; {
  device_id/session_id -&amp;gt; gateway_id, connection_id, session_epoch
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registry is &lt;strong&gt;ephemeral routing information&lt;/strong&gt;, not the source of truth for messages. One user can have several devices and several sessions. A stale registry entry may send a delivery attempt to a dead connection; the gateway can reject it, the routing layer can re-check the route, and durable sync remains the fallback.&lt;/p&gt;

&lt;p&gt;Session epochs or equivalent generation identifiers prevent old sockets from silently replacing newer connections. Gateways must also drain connections during deployments and bound their outbound queues. If Bob's laptop stops reading, retaining unlimited pending messages in gateway memory is not reliable delivery; it is an eventual memory incident. Disconnect the slow session when necessary and let it recover from durable storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Define the message-send contract
&lt;/h2&gt;

&lt;p&gt;Alice generates a stable &lt;code&gt;client_message_id&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; her first send attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message.send"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conversation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"conv-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice-phone-unique-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello Bob"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server authenticates Alice, checks that she can send to the conversation, validates the request, and atomically resolves the client-generated request identity to one canonical server message ID and one conversation-order position. It then commits the message according to its durable-write contract and returns an acceptance acknowledgement.&lt;/p&gt;

&lt;p&gt;The sender acknowledgement means &lt;strong&gt;the server durably accepted the logical message&lt;/strong&gt;. It does not mean Bob's device received it, the message was read, a push notification succeeded, or every downstream fan-out task finished.&lt;/p&gt;

&lt;p&gt;The idempotency mapping might be scoped by &lt;code&gt;(sender_id, sender_device_id, client_message_id)&lt;/code&gt;; its precise scope and retention policy are part of the protocol. Retrying with the same ID but different content should be rejected or handled under an explicitly defined policy, not silently treated as a new message.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lost-ACK problem
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice                     Messaging Service              Durable Store
  |  SEND (client_id=123)        |                            |
  |-----------------------------&amp;gt;|--- atomic accept/write ---&amp;gt;|
  |                              |&amp;lt;--------- committed -------|
  |   ACK is lost   X &amp;lt;----------|
  |                              |                            |
  |  RETRY (client_id=123)       |                            |
  |-----------------------------&amp;gt;|--- resolve same identity --&amp;gt;|
  |&amp;lt;--- original message_id -----|                            |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A naïve &lt;code&gt;SELECT&lt;/code&gt; followed by &lt;code&gt;INSERT&lt;/code&gt; is insufficient: two concurrent requests can both observe that the ID does not exist. Use a uniqueness constraint, conditional write, transaction, or equivalent atomic mechanism. The deduplication record, canonical message identity, and ordering decision must be coordinated so a crash does not create two accepted versions of the same logical send.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;exactly-once logical acceptance&lt;/strong&gt;, under the defined idempotency scope and retention assumptions—not exactly-once packet delivery. Network sends, event deliveries, and client replays can still happen more than once.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Ordering is per conversation, not global
&lt;/h2&gt;

&lt;p&gt;If Alice and Bob send at nearly the same time, their devices may observe different network arrival orders. Timestamps are not a reliable strict-order mechanism because clocks differ and concurrent requests race.&lt;/p&gt;

&lt;p&gt;For this design, an ordering authority assigns each accepted message a monotonically increasing conversation sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conversation conv-42
  seq 418 -&amp;gt; Alice: Hello
  seq 419 -&amp;gt; Bob: Hi
  seq 420 -&amp;gt; Alice: Are you free?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible implementations include an ordered log partition, a single conversation owner/sequencer protected by fencing, or an appropriate storage-side conditional allocation mechanism. Consistent hashing can help route requests to an owner, but &lt;strong&gt;routing to the same instance is not proof of exclusive write authority&lt;/strong&gt; when instances fail or ownership changes.&lt;/p&gt;

&lt;p&gt;Strict &lt;em&gt;contiguous&lt;/em&gt; numbering is a stronger promise than merely increasing order. If we promise contiguous committed sequences, the sequence allocation and durable acceptance protocol must not leave permanent unexplained holes after crashes. An implementation can instead define visible committed order separately from provisional reservations. The important point is to state which contract the client may rely on.&lt;/p&gt;

&lt;p&gt;Suppose Bob receives sequence 420 before 419. His client tracks the highest &lt;strong&gt;contiguous, safely applied&lt;/strong&gt; sequence, buffers newer events within limits, and requests the missing range. It should not advance its checkpoint past an unresolved gap simply because 420 arrived. If targeted repair fails, it can fetch conversation history or reconcile through device sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Persist first; deliver afterward
&lt;/h2&gt;

&lt;p&gt;The central write path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client send
   |
   v
Authenticate + authorize + deduplicate
   |
   v
Assign canonical identity and conversation order
   |
   v
Durable message acceptance
   |                    \
   v                     v
Sender ACK          Durable delivery/sync work
                              |
                     Online device routing
                              |
                     Offline catch-up / push hint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful write must not be followed by a best-effort event publish that can disappear forever if the process crashes. If delivery workers or the sync feed depend on an event stream, use a &lt;strong&gt;transactional outbox, storage-derived change feed, or another recoverable handoff&lt;/strong&gt; with appropriate atomicity and replay guarantees. Otherwise the database can contain an accepted message that downstream systems never learn about.&lt;/p&gt;

&lt;p&gt;The event bus might be Kafka, but Kafka is not mandatory for every messaging architecture. It can handle asynchronous group fan-out, notification work, receipt propagation, and operational events. If a feature relies on Kafka, an outage delays that feature. If Kafka is on the synchronous acceptance path, its availability also affects sender ACKs. Both designs are possible; the dependency boundary must be explicit.&lt;/p&gt;

&lt;p&gt;Consumers should tolerate at-least-once event processing, maintain idempotent effects, and track lag. In-memory producer buffers alone do not provide durable outage recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Offline delivery and multi-device sync
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjsh0ohrliqoehh32b3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjsh0ohrliqoehh32b3j.png" alt="offline delivery" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwqozde69y8u8em1ka4ym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwqozde69y8u8em1ka4ym.png" alt="multi-device sync" width="799" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bob's laptop is online, so the system can attempt immediate delivery to that session. Bob's phone is offline, so there is no usable live route. Neither condition changes whether Alice's message was durably accepted.&lt;/p&gt;

&lt;p&gt;When Bob's phone reconnects, it authenticates and presents its &lt;strong&gt;own durable sync cursor&lt;/strong&gt;. The server returns changes after that checkpoint: messages, membership changes, relevant receipts, and other supported conversation events. The device applies each batch safely, deduplicates by stable identity, and advances its checkpoint only after successful local application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bob laptop: cursor 847  ── receives new event ──&amp;gt; 848
Bob phone:  cursor 842  ── offline
Bob phone reconnects ── fetch events after 842 ──&amp;gt; apply ──&amp;gt; 848
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A cursor can represent a device-scoped ordered change feed rather than the sequence number of any one conversation. &lt;strong&gt;Conversation sequence and device sync cursor are different ordering domains&lt;/strong&gt;. They should not be treated as interchangeable integers.&lt;/p&gt;

&lt;p&gt;Realtime pushes and mobile notifications are wake-up hints, not authoritative history. A missed WebSocket event or failed APNs/FCM notification must not prevent the next sync from discovering the accepted message.&lt;/p&gt;

&lt;p&gt;A device offline beyond the retained incremental change history may have an expired cursor. The server must explicitly require a snapshot or full reconciliation path, respecting deletion, membership, and retention semantics. No design can promise recovery of content already removed under a valid retention or deletion policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Sent, delivered, and read are separate events
&lt;/h2&gt;

&lt;p&gt;A useful state model is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8gqwbi8y0dmmzn8xi9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8gqwbi8y0dmmzn8xi9n.png" alt="states" width="799" height="382"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLIENT_PENDING -&amp;gt; SERVER_ACCEPTED -&amp;gt; RECIPIENT_DELIVERED -&amp;gt; READ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These labels are product semantics, not automatic network guarantees.&lt;/p&gt;

&lt;p&gt;In this illustrative design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server accepted:&lt;/strong&gt; the canonical message has passed the durable write contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivered:&lt;/strong&gt; at least one eligible recipient device has acknowledged that the app received and safely accepted the message locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; a recipient action or client state meets the product's defined read criterion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A gateway successfully writing bytes to a socket is not sufficient evidence that the recipient application processed them. Likewise, delivered-to-one-device and delivered-to-all-devices are different possible products. We choose the former for this example and track richer per-device state internally if needed.&lt;/p&gt;

&lt;p&gt;Read state can often be represented by a monotonic &lt;code&gt;read_up_to_sequence&lt;/code&gt; for each user and conversation, provided the product defines what that high-watermark means. Large groups may aggregate, restrict, or omit detailed per-member read receipts because otherwise receipt traffic can overwhelm message traffic.&lt;/p&gt;

&lt;p&gt;Receipt events can be late or duplicated. Processing must be idempotent and must not regress a valid later state to an earlier one.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Group messaging changes the cost model
&lt;/h2&gt;

&lt;p&gt;One-to-one delivery has a small number of recipients. A group send may need to notify hundreds or thousands of devices; a broadcast channel may have an audience orders of magnitude larger.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3w7udvprmqcgiynsu6o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3w7udvprmqcgiynsu6o.png" alt="group message and fan-out" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two common distribution strategies are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write fan-out:&lt;/strong&gt; create recipient-oriented delivery or inbox work when a message is accepted. This makes individual catch-up and notifications convenient but amplifies writes with audience size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read fan-out:&lt;/strong&gt; store the message once in a canonical conversation or channel log, then let members fetch it. This reduces per-message write amplification but shifts cost toward reads, indexes, and caching.&lt;/p&gt;

&lt;p&gt;A hybrid design often makes sense: a canonical conversation log plus recipient-oriented sync indexes for interactive chats, while very large broadcast channels use store-once, notify-and-fetch behavior. There is no universal group-size cutoff that decides the architecture.&lt;/p&gt;

&lt;p&gt;Group membership is also a correctness problem. If Carol is removed while Alice sends a message, which membership version applies? Bind acceptance and fan-out to a defined membership snapshot or equivalent serialized rule. Otherwise different workers may produce different recipient sets for the same message. Encryption-enabled groups add key-management and membership-epoch concerns on top of that authorization decision.&lt;/p&gt;

&lt;p&gt;The sender's ACK should not wait for thousands of devices to acknowledge delivery. Durable acceptance and downstream group fan-out are separate operations, with explicit lag monitoring and replay behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Store messages for ordered reads and bounded partitions
&lt;/h2&gt;

&lt;p&gt;The durable message store needs high write throughput, efficient history pagination, and access by conversation and sequence. One possible physical layout is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Partition key: (conversation_id, bucket_id)
Sort key:      message_sequence
Value:         message_id, sender_id, payload/ciphertext, metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buckets may use sequence ranges, time windows, or bounded-size policies. This avoids an indefinitely growing physical partition for a long-lived conversation, though &lt;strong&gt;bucketing does not automatically fix a hot current bucket&lt;/strong&gt;. Very active conversations can still overload their current storage replicas or ordering authority.&lt;/p&gt;

&lt;p&gt;A relational database can be suitable at modest scale, particularly when transactional requirements dominate. At extreme append volume, a wide-column or distributed key-value store may better match partition-local history reads. Cassandra, ScyllaDB, DynamoDB, and relational systems have different conditional-write, indexing, transaction, and consistency characteristics; choosing a brand does not itself prove the protocol correct.&lt;/p&gt;

&lt;p&gt;The canonical &lt;strong&gt;conversation log&lt;/strong&gt; and a per-user or per-device &lt;strong&gt;sync index&lt;/strong&gt; are different views. If a sync index is derived asynchronously, the durable handoff and replay path must ensure that acknowledged messages eventually become discoverable by authorized devices, subject to retention and availability constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Presence, typing, and push notifications are not durable messages
&lt;/h2&gt;

&lt;p&gt;Presence is usually derived from active sessions with heartbeat-refreshed leases or TTLs. It is approximate, affected by mobile background behavior, and subject to privacy rules. If any permitted active session is live, a product may show a user as online—but this is a policy, not a universally accurate fact.&lt;/p&gt;

&lt;p&gt;Typing indicators are short-lived events that can be coalesced, rate-limited, or dropped under load. Persisting a backlog of stale typing events adds cost without value.&lt;/p&gt;

&lt;p&gt;Mobile push notifications are also best-effort wake-up mechanisms. They may be delayed, duplicated, suppressed, or unavailable. A push payload must respect privacy and, where enabled, end-to-end encryption constraints. The client still fetches authoritative state from the messaging service after waking.&lt;/p&gt;

&lt;p&gt;These workloads should degrade before the system sacrifices the integrity of already accepted messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Media messages take a separate byte path
&lt;/h2&gt;

&lt;p&gt;Sending a photo should not mean streaming megabytes through the same gateway buffers that handle text messages and receipts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxc91wh3cbeaj0kvo44u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxc91wh3cbeaj0kvo44u.png" alt="media architecture" width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender ── authorized upload ──&amp;gt; Media Service / Object Storage
   |
   └── message.send(media reference) ──&amp;gt; Message Service
                                          |
Recipient &amp;lt;── authorized download / CDN &amp;lt;─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client uploads the media through an authorized, potentially resumable object-storage path. The message contains a reference and the metadata needed for retrieval. The service must define when that reference is considered valid, how orphaned uploads are cleaned up, and how access is checked on download.&lt;/p&gt;

&lt;p&gt;For private content, a CDN requires its &lt;strong&gt;own&lt;/strong&gt; appropriate access-control strategy; an object-storage presigned URL is not automatically a secure CDN authorization scheme. Media can fail independently of text chat, and clients should surface that failure rather than treating a missing upload as a successfully delivered attachment.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. End-to-end encryption changes the trust model
&lt;/h2&gt;

&lt;p&gt;End-to-end encryption (E2EE) means the service should not possess the plaintext keys needed to read protected message content under the chosen threat model and protocol. Servers still need to authenticate users and devices, route ciphertext, store encrypted payloads, coordinate device enrollment, and manage operational metadata.&lt;/p&gt;

&lt;p&gt;Multiple devices and groups make key distribution more complicated. Adding or removing a device, changing group membership, and restoring backups all need explicit cryptographic policies. Encryption also constrains server-side search, moderation, and content-derived analytics.&lt;/p&gt;

&lt;p&gt;E2EE is not achieved merely by storing encrypted strings in a database or enabling TLS. A real deployment requires a reviewed cryptographic protocol, secure client key handling, device verification and recovery semantics, and careful metadata-leakage analysis. This article treats E2EE as an architectural constraint rather than inventing a complete secure messaging protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. What happens when components fail?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The gateway crashes.&lt;/strong&gt; Connected clients lose their sockets, reconnect with backoff and jitter, register new routes, and resume from durable sync cursors. An accepted message is not dependent on the dead gateway's memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alice's ACK is lost.&lt;/strong&gt; She retries using the same client-generated ID. Atomic idempotency returns the original canonical message and order, rather than writing a duplicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bob receives the same event twice.&lt;/strong&gt; His device deduplicates by canonical message ID and applies the event idempotently. At-least-once delivery is compatible with one logical chat bubble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bob receives sequence 103 before 102.&lt;/strong&gt; His client detects the gap, buffers within a limit, fetches missing history, and does not advance a contiguous checkpoint incorrectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The message store cannot meet its write contract.&lt;/strong&gt; The service must not send a success ACK for an uncommitted message. Clients can retry safely, and already acknowledged data retains only the durability guarantee the chosen storage configuration actually provides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kafka or a delivery worker is down.&lt;/strong&gt; Kafka-dependent fan-out, push, or receipt processing may lag. Recoverable outbox or change-feed state preserves pending work, and workers replay idempotently after recovery. If Kafka is on the acceptance path, new sends may also be blocked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Millions of devices reconnect together.&lt;/strong&gt; Gateways, authentication, registries, and sync APIs face a recovery surge. Use jittered reconnect, admission control, bounded catch-up batches, and capacity headroom rather than allowing recovery to create a second outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One conversation becomes extremely hot.&lt;/strong&gt; Current-bucket write pressure and sequencer contention can persist even with historical bucketing. Apply workload-aware limits or specialized handling while preserving the stated ordering contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A region fails.&lt;/strong&gt; Cross-region routing alone is not enough. The system must define replication lag, recovery point and time objectives, and how a new conversation writer gains authority without a stale writer continuing to accept conflicting sequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Multi-region messaging and split-brain prevention
&lt;/h2&gt;

&lt;p&gt;A defensible baseline gives each conversation one current write authority, often associated with a home region. Users may connect to nearby gateways around the world, but accepted writes for that conversation pass through its authoritative ordering path. Other regions can handle routing, caching, delivery, and replication.&lt;/p&gt;

&lt;p&gt;Active-active writes across regions are possible, but require a more sophisticated ordering or conflict contract. Two regions must not independently claim to own the same strict conversation sequence during a network partition. Lease ownership must be backed by effective fencing or another mechanism that rejects stale writers; a lease timestamp alone is not sufficient if old owners can still write.&lt;/p&gt;

&lt;p&gt;Cross-region replication may be asynchronous, which means regional disaster recovery can involve a nonzero recovery point objective. Claiming both immediate local writes everywhere and strict global per-conversation ordering without explaining the coordination cost would be misleading.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. Putting the architecture together
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6hybslizsbbsq8kcu0xd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6hybslizsbbsq8kcu0xd.png" alt="whatsapp final architecture" width="799" height="432"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Mobile / Web / Desktop Clients
                              |
                       Load Balancer
                              |
                    WebSocket Gateways
                      |           |
                      |      Connection Registry
                      v
                  Messaging Service
                (auth, idempotency,
                conversation ordering)
                      |
                Durable Message Store
                      |
          Durable Outbox / Change Feed
                      |
               Event Stream / Workers
                 /       |         \
          Live fan-out  Sync index   Push hints
                |          |           |
             Gateways   Sync API   APNs / FCM
                           |
                       Reconnecting
                         devices

       Separate: conversation/membership metadata,
       presence/typing, media object storage + CDN,
       key/device management if E2EE is enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Return to Alice and Bob. Alice sends M with a stable client ID. The service authorizes the send, assigns a canonical message identity and conversation order, commits the durable message and its recoverable downstream handoff, then attempts to ACK Alice. The ACK disappears, but the write does not. Alice retries, and the service returns the original result.&lt;/p&gt;

&lt;p&gt;Bob's connected laptop receives M and acknowledges receipt according to the product's delivery rule. Bob's offline phone later reconnects and obtains M through its own sync cursor. If the laptop gets a duplicate event, it ignores the duplicate logical message. If the phone discovers a sequence gap, it repairs it from authoritative history. None of these cases requires pretending that a WebSocket is a durable queue.&lt;/p&gt;

&lt;p&gt;The design succeeds because it draws precise boundaries: &lt;strong&gt;durable acceptance is not device delivery; device delivery is not reading; live routing is not offline recovery; and per-conversation order is not global order&lt;/strong&gt;. Once those contracts are explicit, component choices become much easier to evaluate.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;p&gt;For deeper explanations of the design decisions and their failure boundaries, see SeeItFlow's Messaging Platform module:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/whatsapp/complete-design" rel="noopener noreferrer"&gt;Messaging Platform — Complete Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/whatsapp/visual-design-journey" rel="noopener noreferrer"&gt;Messaging Platform — Visual Journey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/whatsapp/architecture-deep-dive" rel="noopener noreferrer"&gt;Messaging Platform — Architecture Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/whatsapp/tradeoffs-decisions" rel="noopener noreferrer"&gt;Messaging Platform — Trade-offs &amp;amp; Decisions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/whatsapp/failure-scale-scenarios" rel="noopener noreferrer"&gt;Messaging Platform — Failure &amp;amp; Scale Scenarios&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/whatsapp/interview-perspective" rel="noopener noreferrer"&gt;Messaging Platform — Interview Perspective&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>distributed</category>
      <category>backend</category>
    </item>
    <item>
      <title>Designing Dropbox: File Storage &amp; Sync System Design</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:55:50 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/designing-dropbox-file-storage-sync-system-design-o2k</link>
      <guid>https://dev.to/mangeshmandlik/designing-dropbox-file-storage-sync-system-design-o2k</guid>
      <description>&lt;p&gt;&lt;em&gt;Uploading a file is straightforward. Keeping its bytes, metadata, permissions, history, and copies on several intermittently connected devices consistent is the real system-design problem.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzv4a8mz0k9yzfwy059f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzv4a8mz0k9yzfwy059f.png" alt="complete architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Consider a document named &lt;code&gt;report.docx&lt;/code&gt;. You edit it on your laptop, close the lid halfway through an upload, rename its folder on another device, and then open your phone after several days offline. Meanwhile, a collaborator may have edited the same document or lost access to it.&lt;/p&gt;

&lt;p&gt;A file storage and synchronization system must answer more than “where do we store the bytes?” It must know &lt;strong&gt;which version is current, which changes each device has observed, what to do when histories diverge, and whether a user is still authorized to download a particular version&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We'll design a Dropbox- or Google Drive-like system from a simple upload API to a reliable sync architecture. This is a conceptual design, not a claim about either company's internal implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements and scale
&lt;/h2&gt;

&lt;p&gt;The system supports uploading and downloading large files, resumable transfers, folders, rename and move, automatic synchronization across devices, offline edits, version history, deletion recovery, and viewer/editor sharing.&lt;/p&gt;

&lt;p&gt;The priorities are durability, integrity, secure access, availability despite individual failures, reasonable synchronization freshness, and bandwidth efficiency. “Instant sync” and “never lose a file” are not defensible unconditional guarantees: network partitions, retention rules, disasters, and client availability impose real limits.&lt;/p&gt;

&lt;p&gt;For a scale exercise, suppose we have &lt;strong&gt;500 million registered users, 100 million daily active users, and 200 million uploads per day&lt;/strong&gt;. With an illustrative average upload of &lt;strong&gt;5 MB&lt;/strong&gt;, that's approximately &lt;strong&gt;1 PB of new uploaded data per day&lt;/strong&gt; before compression, deduplication, replication, or retention effects. The mean arrival rate is about &lt;strong&gt;2,315 uploads/second&lt;/strong&gt;, but peak capacity must be sized above the mean. Assume individual files can reach &lt;strong&gt;50 GB&lt;/strong&gt;. These are invented design inputs, not published statistics for Dropbox or Google Drive.&lt;/p&gt;

&lt;p&gt;Notice that upload &lt;em&gt;requests&lt;/em&gt;, bytes ingested, metadata operations, download traffic, and device sync connections scale differently. We should not expect one service or database to solve them all.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Start with the simplest upload architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client ── file bytes ──&amp;gt; Application server ──&amp;gt; Local disk
                             │
                             └───────────────&amp;gt; Metadata database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdskdfx0m63qeg8zvm4ca.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdskdfx0m63qeg8zvm4ca.png" alt="start simple"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This works for small workloads. But when a large upload is interrupted, a one-shot upload may have to restart. Application servers also spend bandwidth and connection capacity relaying bytes rather than handling application logic. Local disks complicate replication, capacity expansion, and recovery.&lt;/p&gt;

&lt;p&gt;Proxying bytes is not inherently wrong: inspection, transformations, restricted networks, or specialized clients can justify it. For general large-file transfers at our assumed scale, however, &lt;strong&gt;separating the control plane from the byte-transfer path&lt;/strong&gt; is a useful next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Separate file metadata from object storage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                       ┌──────────────────────┐
Client ── API calls ──&amp;gt; │ File / Metadata API  │ ──&amp;gt; Metadata DB
                       └──────────────────────┘      identity, folders,
                                  │                 ACLs, versions, state
                                  │ signed capability
                                  ▼
Client ── file bytes directly ──&amp;gt; Object storage
                                 immutable version objects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxp3g1kz97e1fd6po2ibr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxp3g1kz97e1fd6po2ibr.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A metadata record might contain &lt;code&gt;file_id&lt;/code&gt;, &lt;code&gt;owner_id&lt;/code&gt;, &lt;code&gt;parent_folder_id&lt;/code&gt;, &lt;code&gt;filename&lt;/code&gt;, &lt;code&gt;current_version_id&lt;/code&gt;, &lt;code&gt;lifecycle_state&lt;/code&gt;, and a concurrency token. A separate versions table stores &lt;code&gt;version_id&lt;/code&gt;, &lt;code&gt;file_id&lt;/code&gt;, &lt;code&gt;parent_version_id&lt;/code&gt;, content checksum, size, and object key. The file's &lt;strong&gt;stable ID is independent of its path&lt;/strong&gt;: renaming &lt;code&gt;report.docx&lt;/code&gt; or moving it into another folder changes metadata, not the file's identity or necessarily its stored bytes.&lt;/p&gt;

&lt;p&gt;Folders also have stable IDs and parent relationships. Listing a folder means querying its immediate children with pagination. A folder move can change the folder's parent pointer without rewriting every descendant, although subtree permissions, path caches, and asynchronous indexes may still require careful handling. If sibling names must be unique, enforce that invariant within the correct namespace, including the root-folder case.&lt;/p&gt;

&lt;p&gt;Object storage holds large binary objects; the metadata store handles queryable relationships and conditional updates. This is a &lt;strong&gt;workload-driven decision&lt;/strong&gt;, not a rule that databases can never store small blobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The metadata–object consistency gap
&lt;/h3&gt;

&lt;p&gt;The database transaction that publishes a file version cannot ordinarily atomically include the object-storage upload. Therefore, use an explicit lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATED → UPLOADING → VERIFYING → AVAILABLE
                ↘ FAILED / EXPIRED
AVAILABLE → DELETING → DELETED (subject to retention)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safe publication sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a metadata record and an upload session in a non-available state.&lt;/li&gt;
&lt;li&gt;Upload bytes to a server-selected object key.&lt;/li&gt;
&lt;li&gt;Complete the object upload and verify the integrity evidence supported by the storage API.&lt;/li&gt;
&lt;li&gt;Atomically update the file's current-version pointer &lt;strong&gt;and record its sync change&lt;/strong&gt; using a transaction or another durable, correctly ordered publication mechanism.&lt;/li&gt;
&lt;li&gt;Reconcile abandoned sessions and orphan objects asynchronously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fourth step matters. If a file becomes visible in metadata but its corresponding change event is lost, other devices may never discover it. A &lt;strong&gt;transactional outbox&lt;/strong&gt; or a change log committed alongside metadata can bridge that boundary; publishing to an unrelated queue after the database commit without recovery is not enough.&lt;/p&gt;

&lt;p&gt;If storage succeeds but the metadata commit fails, the object may be orphaned. A reconciler checks the session, object, checksum, and current metadata state before safely retrying publication or scheduling deletion after a grace period. If metadata says &lt;code&gt;AVAILABLE&lt;/code&gt; but the object is missing, fail visibly and investigate or recover from a replica/backup; never serve an empty object as success.&lt;/p&gt;

&lt;p&gt;For deeper treatment, see &lt;a href="https://seeitflow.com/system-design/foundations/filestorage/architecture-deep-dive" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Architecture Deep Dive&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Design the upload API and idempotency contracts
&lt;/h2&gt;

&lt;p&gt;An illustrative API is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create file metadata; client supplies an idempotency key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /files/{id}/uploads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start or resume an upload session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /files/{id}/complete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complete and verify a session; safe to retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /files/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return metadata and current version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /files/{id}/download&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authorize and issue a short-lived download capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATCH /files/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rename/move with a version precondition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /sync/changes?cursor=...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch durable changes for a device&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are example contracts, not a mandated REST standard. A production implementation must also define deletion, restore, sharing, version retrieval, and session-status endpoints.&lt;/p&gt;

&lt;p&gt;A retry of &lt;code&gt;POST /files&lt;/code&gt; must not accidentally create two files. An &lt;strong&gt;idempotency key scoped to the caller and operation&lt;/strong&gt; allows the server to return the same logical result. Completing an upload should similarly be retry-safe: a timeout may mean “completion succeeded but the response was lost.” Repeating that request must not publish a duplicate version.&lt;/p&gt;

&lt;p&gt;Idempotency handles retries of the &lt;em&gt;same&lt;/em&gt; operation. It does not solve two genuinely concurrent edits; those require version preconditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Direct uploads and presigned URL security
&lt;/h2&gt;

&lt;p&gt;For the normal large-file path, the application authenticates the caller, checks permission and quota, creates the session, and returns a short-lived presigned upload URL. The client sends bytes directly to object storage.&lt;/p&gt;

&lt;p&gt;A presigned URL is generally a &lt;strong&gt;bearer capability&lt;/strong&gt;. Anyone who obtains it can potentially use it until expiration, subject to signature and storage-policy constraints. It is &lt;strong&gt;not automatically bound to the original user's identity&lt;/strong&gt;. Therefore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authorize &lt;em&gt;before&lt;/em&gt; issuing it; generate object keys on the server.&lt;/li&gt;
&lt;li&gt;Scope it to a specific operation and object or multipart part.&lt;/li&gt;
&lt;li&gt;Use HTTPS, short expirations, appropriate request constraints where supported, and avoid leaking URLs in logs.&lt;/li&gt;
&lt;li&gt;Configure browser CORS separately; CORS is not a replacement for authorization.&lt;/li&gt;
&lt;li&gt;Recheck authorization and the expected version before publishing a completed upload, because permissions or file state may have changed while bytes were in flight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revoking a user's ACL does &lt;strong&gt;not automatically revoke a previously issued, unexpired URL&lt;/strong&gt;. If near-immediate revocation is mandatory, an edge or proxy path that validates current authorization on access may be necessary. Even then, bytes already downloaded cannot be recalled.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Resumable multipart uploads
&lt;/h2&gt;

&lt;p&gt;A single 10 GB PUT still has an expensive failure mode: a dropped connection can force a complete restart. Native object-storage multipart upload lets the client send separately acknowledged parts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload session U17
  part 1 ──────── ✓
  part 2 ──────── ✓
  part 3 ── X     retry part 3
  part 4 ──────── ✓
                ↓
  complete known parts → verify → publish new version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmecayjmhuughcq10nnri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmecayjmhuughcq10nnri.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A durable session tracks its owner, file ID, expected base version, storage upload ID, expiration, and status. The client can upload parts concurrently and, after reconnecting, &lt;strong&gt;reconcile confirmed parts against the storage provider's actual state&lt;/strong&gt; where supported. Its own local progress counter may be stale if a part succeeded but the response was lost.&lt;/p&gt;

&lt;p&gt;Part size is a trade-off: smaller parts reduce failed-retry cost but increase request and tracking overhead; larger parts reduce coordination but increase retry cost and buffering. Provider minimums, maximum part counts, file size, client memory, and network behavior determine the right choice. Don't treat a single part size as universal across providers.&lt;/p&gt;

&lt;p&gt;An object-store multipart part is not necessarily the same abstraction as an application-level delta-sync chunk. Native multipart simplifies upload assembly; a content-addressed chunk store is a separate, substantially more complex design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrity is not the same as upload completion
&lt;/h3&gt;

&lt;p&gt;TLS protects bytes in transit, but it does not prove that the application selected the correct file. Where supported, validate per-part checksums and the assembled object's expected checksum. A mismatch in the final whole-object hash shows that something differs; &lt;strong&gt;it does not identify the corrupted part&lt;/strong&gt; without finer-grained evidence.&lt;/p&gt;

&lt;p&gt;Do not assume an object-storage &lt;code&gt;ETag&lt;/code&gt; is always an MD5 digest of the entire file. ETag semantics vary by provider and upload/encryption method, especially for multipart objects. Use explicit checksum mechanisms and clearly specify what has been verified before publication.&lt;/p&gt;

&lt;p&gt;Expired sessions and abandoned multipart uploads require cleanup. Unassembled parts may still consume storage and cost money; expiry policies and periodic reconciliation are part of the design, not optional housekeeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Downloads, range requests, and private CDN delivery
&lt;/h2&gt;

&lt;p&gt;For downloads, the file API checks the current ACL and returns a short-lived signed capability for a specific immutable file version. The client downloads bytes from storage, or through an appropriately secured CDN where caching is useful.&lt;/p&gt;

&lt;p&gt;HTTP &lt;code&gt;Range&lt;/code&gt; requests can resume an interrupted download or retrieve part of a large file. A robust client should ensure the resumed ranges belong to the &lt;strong&gt;same immutable object version&lt;/strong&gt; and verify the completed file's expected checksum. Simply appending bytes from a mutable URL can mix different versions.&lt;/p&gt;

&lt;p&gt;CDN caching is workload-dependent. Frequently requested, immutable shared objects may benefit; private files downloaded once per user may not. A private CDN needs &lt;strong&gt;its own edge authorization and private-origin configuration&lt;/strong&gt;. A presigned origin-storage URL does not magically make a CDN path private or authorized. Already-issued CDN capabilities have similar revocation windows to signed origin URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Multi-device sync is a reconciliation protocol
&lt;/h2&gt;

&lt;p&gt;A sync client needs persistent local state, not merely a filesystem watcher. For each file it tracks the stable remote ID, local path, last synchronized server version, local fingerprint, pending operations, and its durable checkpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Laptop: edit report.docx
  local state: LOCAL_DIRTY
           ↓
  upload candidate version based on v5
           ↓
  server checks current version == v5
           ↓
  publish v6 + durable change record
           ↓
  push hint to other online devices
           ↓
Phone: GET /sync/changes?cursor=C
           ↓
  apply v6, then advance local cursor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local filesystem notifications can be missed, so a periodic reconciliation scan is a useful safety net. Server push via WebSocket, mobile push, or long polling is &lt;strong&gt;only a wake-up hint&lt;/strong&gt;. The durable, cursor-based change log is the source of truth.&lt;/p&gt;

&lt;p&gt;A cursor identifies a position in an ordered change stream &lt;strong&gt;within its defined scope&lt;/strong&gt;. It is not simply the client's wall-clock timestamp. The client must not persist a cursor beyond changes it has durably applied; otherwise a crash can permanently skip an update. Applying duplicate events must be safe through operation IDs, version checks, and idempotent local updates.&lt;/p&gt;

&lt;p&gt;One subtlety: when changes are paginated or generated concurrently, the server must define a consistent ordering/checkpoint contract so clients cannot skip a late-committed entry with an earlier cursor position. A snapshot-plus-change-stream handoff likewise needs a defined boundary; “take a snapshot, then start polling” without such a boundary can miss mutations.&lt;/p&gt;

&lt;p&gt;If a device has been offline longer than change-log retention, its old cursor may be invalid. It must obtain a consistent namespace snapshot, compare it with local state, reconcile offline operations, and establish a fresh cursor without losing changes during the transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Whole-file hashing is not delta synchronization
&lt;/h2&gt;

&lt;p&gt;A content hash answers: &lt;strong&gt;are these bytes equal?&lt;/strong&gt; It does not answer: &lt;strong&gt;which blocks changed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are three practical strategies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Works well when&lt;/th&gt;
&lt;th&gt;Main limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whole-file replacement&lt;/td&gt;
&lt;td&gt;Files are small or binary deltas are ineffective&lt;/td&gt;
&lt;td&gt;Transfers the entire new file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed-size block comparison&lt;/td&gt;
&lt;td&gt;Large files change in place without shifting offsets&lt;/td&gt;
&lt;td&gt;Insertions shift later block boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rolling-signature or content-defined chunking&lt;/td&gt;
&lt;td&gt;Large files have localized edits and reusable regions&lt;/td&gt;
&lt;td&gt;More CPU, signatures, chunk metadata, and GC complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An rsync-style rolling matching algorithm and content-defined chunking are related approaches, &lt;strong&gt;not identical algorithms&lt;/strong&gt;. Both can locate reusable regions despite offset shifts, but their chunking and matching procedures differ.&lt;/p&gt;

&lt;p&gt;For compressed archives, encrypted data, and some document formats, a tiny logical edit can change much of the serialized byte stream. In those cases, a delta algorithm may save little bandwidth. The client should choose based on measured transfer savings versus computation and coordination cost, not promise that every one-paragraph edit uploads only a few kilobytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Detect conflicts using version lineage
&lt;/h2&gt;

&lt;p&gt;Suppose both laptop and phone begin with &lt;code&gt;report.docx&lt;/code&gt; at version &lt;code&gt;v5&lt;/code&gt;. The laptop edits and successfully publishes &lt;code&gt;v6&lt;/code&gt;. The phone was offline and independently edited its copy based on &lt;code&gt;v5&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             v5 (shared base)
             /            \
   laptop candidate     phone candidate
        accepted v6       base=v5
             │              │
       current=v6      compare-and-set fails
                            │
                      preserve as conflict
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server accepts a new current version only when its &lt;strong&gt;expected base version matches the current version&lt;/strong&gt;. A stale writer must not silently overwrite the newer version. This check must be atomic at the metadata commit boundary.&lt;/p&gt;

&lt;p&gt;Possible policies include preserving a conflict copy, last-write-wins, or a three-way merge for supported text formats. Conflict copies reduce silent overwrites but require user intervention and do not guarantee zero data loss if the underlying storage or backup system fails. Last-write-wins is simple but can discard edits. Automatic merging is format-dependent; it is not a general solution for arbitrary binary files. Real-time collaborative editing with OT or CRDTs is a different problem from background file synchronization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata edits also race.&lt;/strong&gt; Rename and move operate on stable IDs; delete should produce a tombstone that offline devices can observe. When a deleted file receives a stale offline edit, the product must explicitly choose to reject it, restore a conflict copy, or create a recovered file. A naive sync client that sees a missing file and uploads its old local copy risks resurrecting a deletion.&lt;/p&gt;

&lt;p&gt;Content version checks and metadata-operation concurrency tokens may need separate treatment: a rename need not create a new content blob, yet it must still be protected against conflicting namespace mutations.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Sharing and access control
&lt;/h2&gt;

&lt;p&gt;The metadata layer records ownership, direct shares, and possibly inherited folder permissions. Each operation checks whether the caller may read, edit, rename, delete, share, or restore the target. Folder inheritance and override rules must be defined rather than guessed.&lt;/p&gt;

&lt;p&gt;For a file shared by Alice with Bob, Bob requests a download through the application, which checks Bob's access and issues a scoped signed URL. Direct object-store access &lt;strong&gt;after&lt;/strong&gt; that check is intentional. The file bytes need not pass through the authorization service.&lt;/p&gt;

&lt;p&gt;A recipient-oriented index for “shared with me” avoids scanning every owner's ACLs as metadata is sharded. That index must be maintained carefully, but &lt;strong&gt;an eventually updated discovery index must not become the sole authority for granting access&lt;/strong&gt;. Authorization-sensitive paths should check authoritative permission state or an explicitly safe consistency mechanism. If current authorization cannot be determined, fail closed.&lt;/p&gt;

&lt;p&gt;Malware scanning can also be a security gate. If the product requires scanning before sharing or downloading, a failed scanner must leave the file quarantined or pending, not silently mark it usable. Thumbnail generation and search indexing, by contrast, can often fail without making the file itself unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Scaling the metadata and sync planes
&lt;/h2&gt;

&lt;p&gt;Object storage handles bulk bytes; the metadata database handles frequently changing file, folder, permission, and version records. Read replicas may help appropriate read workloads, but stale replicas are unsafe for decisions requiring the latest ACL or concurrency state.&lt;/p&gt;

&lt;p&gt;Owner-based sharding keeps a user's namespace relatively local. Its weakness is a very large or active user/tenant, and cross-owner “shared with me” queries. Tenant-based partitioning improves isolation but can produce giant-tenant hotspots. File-ID sharding spreads load but requires secondary indexes for folder and owner queries. There is no universally best shard key.&lt;/p&gt;

&lt;p&gt;A huge folder should be listed with pagination, not enumerated in a single response. Fan-out notifications and derived indexes should be processed asynchronously through durable jobs. Per-tenant fairness and backpressure protect other users when one tenant generates extreme traffic.&lt;/p&gt;

&lt;p&gt;The sync change stream also has independent limits: write throughput, cursor retention, ordering scope, connected-device count, and notification fan-out. Partitioning by user can help localize ordered processing &lt;strong&gt;if the underlying stream actually provides that ordering&lt;/strong&gt;, but it does not replace operation idempotency or atomic version preconditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Failure scenarios that reveal correctness bugs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Safe behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Upload interrupted&lt;/td&gt;
&lt;td&gt;Reconcile storage-confirmed parts; retry missing parts only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upload succeeds, metadata commit fails&lt;/td&gt;
&lt;td&gt;Idempotent completion plus reconciliation; never publish unverified bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presigned URL expires&lt;/td&gt;
&lt;td&gt;Check session status and refresh URLs for remaining parts if still valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata primary unavailable&lt;/td&gt;
&lt;td&gt;Fail over safely or reject mutations; don't acknowledge writes buffered only in memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage temporarily unavailable&lt;/td&gt;
&lt;td&gt;Bound retries with backoff and jitter; expose degraded byte-transfer state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync push unavailable&lt;/td&gt;
&lt;td&gt;Poll durable change log on reconnect or schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate/out-of-order sync delivery&lt;/td&gt;
&lt;td&gt;Use operation IDs, version preconditions, and safe cursor checkpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device offline beyond cursor retention&lt;/td&gt;
&lt;td&gt;Full snapshot reconciliation with a safe change-stream handoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACL revoked after URL issuance&lt;/td&gt;
&lt;td&gt;Recognize capability expiry window; stricter validation if required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abandoned multipart sessions&lt;/td&gt;
&lt;td&gt;Abort expired sessions and reclaim parts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Garbage collector thinks a live object is orphaned&lt;/td&gt;
&lt;td&gt;Grace period, durable deletion state, reference checks, and independent audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regional outage&lt;/td&gt;
&lt;td&gt;Follow defined RPO/RTO; prevent split-brain writes during failover&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The garbage collector deserves special attention. An object may appear unreferenced because a metadata query failed, a replication lagged, or a deduplication reference has not yet been observed. &lt;strong&gt;Never hard-delete a blob solely because one transient lookup returns no references.&lt;/strong&gt; Use retention windows, tombstones, durable GC state, rechecks, and recoverable backups.&lt;/p&gt;

&lt;p&gt;Cross-user content deduplication is optional, not a free storage win. It complicates reference counting and deletion, and a naive “does this hash already exist?” endpoint can leak information about another user's private data. Start without global deduplication unless measured storage savings justify its security and operational complexity.&lt;/p&gt;

&lt;p&gt;For a scenario-by-scenario breakdown, see &lt;a href="https://seeitflow.com/system-design/foundations/filestorage/failure-scale-scenarios" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Failure &amp;amp; Scale Scenarios&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Architectural trade-offs worth defending
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Direct transfer or application proxy?&lt;/strong&gt; Direct signed upload/download reduces application bandwidth at scale. Proxying remains appropriate for mandatory inline inspection, transformation, or real-time authorization controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native multipart or application-managed chunk store?&lt;/strong&gt; Native multipart simplifies reliable large uploads. A content-addressed chunk store can support cross-version reuse and deduplication but introduces reference-counting, garbage-collection, and privacy complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push or polling?&lt;/strong&gt; Push reduces notification delay for connected clients. Polling is a necessary fallback. Neither replaces the durable change log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong consistency or eventual consistency?&lt;/strong&gt; The &lt;em&gt;acceptance of a new current version&lt;/em&gt; needs a correct atomic precondition. Other devices may observe that committed version later. These are separate consistency boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-region or multi-region?&lt;/strong&gt; A single-region primary with tested backups and a defined disaster-recovery plan is a defensible baseline. Cross-region replication can improve recovery objectives but requires handling replication lag and preventing two primaries from accepting conflicting writes during a partition. Object durability is not the same as an RPO or RTO guarantee.&lt;/p&gt;

&lt;p&gt;For alternatives and decision criteria, see &lt;a href="https://seeitflow.com/system-design/foundations/filestorage/tradeoffs-decisions" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Trade-offs &amp;amp; Decisions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. System design interview follow-ups
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not keep file bytes in PostgreSQL?&lt;/strong&gt; Small blobs may be fine there. At the stated multi-GB-file and petabyte-scale ingest workload, separate object storage better fits byte capacity, transfer, and lifecycle requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when a 50 GB upload fails at 90%?&lt;/strong&gt; Recover the durable multipart session, reconcile parts confirmed by storage, refresh expired upload capabilities if the session is valid, and retry only missing parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the client trust ETag as an MD5?&lt;/strong&gt; No. Provider, multipart, and encryption behavior affect its meaning. Specify explicit checksum verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do devices discover missed changes?&lt;/strong&gt; Fetch from the durable change log using a cursor. Push is a hint; polling and reconnect reconciliation provide recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does SHA-256 identify changed blocks?&lt;/strong&gt; A single whole-file SHA-256 does not. Delta algorithms need block-level or rolling/content-defined signatures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if two offline devices edit the same file?&lt;/strong&gt; Both carry the base version they edited. Atomic compare-and-set accepts one current version; the divergent edit is handled by an explicit conflict policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you instantly revoke an issued presigned URL?&lt;/strong&gt; Not by changing only the file ACL. The already-issued capability may remain usable until expiry unless the access path supports further revocation checks or invalidation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if a device reconnects after six months?&lt;/strong&gt; If its cursor is older than retained history, take a consistent snapshot, reconcile queued local changes, and rejoin the change stream at a safe checkpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you prevent sync from losing a metadata mutation?&lt;/strong&gt; Persist the mutation and its change-log/outbox record atomically or use an equivalent recoverable publication protocol. A best-effort queue publish after commit can lose events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you recover from accidental object deletion?&lt;/strong&gt; Recovery depends on version retention, backups, and provider replication capabilities. A design that promises recovery must test its restore procedure and define acceptable loss and recovery time.&lt;/p&gt;

&lt;p&gt;A larger question bank is available in &lt;a href="https://seeitflow.com/system-design/foundations/filestorage/interview-perspective" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Interview Perspective&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         ┌──────────────────────────┐
Laptop / Phone / Web ───&amp;gt; │ API / Auth / File Service│ ──&amp;gt; Metadata DB
        │                └──────────┬───────────────┘     file IDs, ACLs,
        │                           │                     versions, folders
        │                    signed upload/download
        │                           │
        └──────── file bytes ───────┴───────────────&amp;gt; Object Storage
        │                                           immutable version blobs
        │
        └── Sync client ──&amp;gt; Sync API ──&amp;gt; Durable change log / outbox
                               │                    │
                               └── cursor reads &amp;lt;───┘
                                      │
                           Push hints / polling fallback

              Background workers: scanning, previews, indexing,
                    session reconciliation, retention / GC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important insight is not the number of boxes. It is the &lt;strong&gt;boundaries between guarantees&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Object storage protects and serves bytes; metadata identifies which bytes belong to which logical file version.&lt;/li&gt;
&lt;li&gt;A verified upload is not visible until its version is safely published.&lt;/li&gt;
&lt;li&gt;An accepted edit is conditionally committed; its arrival on other devices is asynchronous.&lt;/li&gt;
&lt;li&gt;Push improves freshness, but durable replay preserves synchronization correctness.&lt;/li&gt;
&lt;li&gt;Authorization gates capability issuance; signed URLs have explicit lifetime and revocation limitations.&lt;/li&gt;
&lt;li&gt;Garbage collection must not destroy data that another component still references.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good file storage and sync design makes those boundaries explicit. That is what turns a basic upload feature into a system that can survive interrupted transfers, offline edits, permission changes, retries, and operational failures without silently corrupting user state.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/filestorage/complete-design" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Complete Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/filestorage/visual-design-journey" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Visual Journey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/filestorage/architecture-deep-dive" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Architecture Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/filestorage/tradeoffs-decisions" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Trade-offs &amp;amp; Decisions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/filestorage/failure-scale-scenarios" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Failure &amp;amp; Scale Scenarios&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/filestorage/interview-perspective" rel="noopener noreferrer"&gt;File Storage &amp;amp; Sync — Interview Perspective&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>backend</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Designing Search &amp; Autocomplete: Inverted Index, Trie, Ranking &amp; Scale</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Sat, 10 Oct 2026 04:10:33 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/designing-search-autocomplete-inverted-index-trie-ranking-scale-13k5</link>
      <guid>https://dev.to/mangeshmandlik/designing-search-autocomplete-inverted-index-trie-ranking-scale-13k5</guid>
      <description>&lt;p&gt;&lt;em&gt;The difficult part of search isn't finding a word. It's returning the right results quickly while documents change, users type, and parts of the search infrastructure fail.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4jwfynfnwc7l3u3899s2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4jwfynfnwc7l3u3899s2.png" alt="complete architecture" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagine an e-commerce catalog with millions of products. A user types &lt;code&gt;iph&lt;/code&gt;, sees suggestions, completes &lt;code&gt;iphone case&lt;/code&gt;, applies a price filter, and expects relevant results almost immediately.&lt;/p&gt;

&lt;p&gt;A database query can support this at small scale. At larger scale, the system must solve several distinct problems: &lt;strong&gt;text retrieval, suggestion generation, ranking, freshness, filtering, and distributed execution&lt;/strong&gt;. Treating all of them as one database lookup makes the design harder to reason about.&lt;/p&gt;

&lt;p&gt;This article develops a search and autocomplete system from requirements through production failure modes. The example is a product catalog, but the underlying techniques also apply to document search, content discovery, and entity search.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements: What Are We Actually Building?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Functional requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Search products by keywords across titles, descriptions, and selected attributes.&lt;/li&gt;
&lt;li&gt;Show autocomplete suggestions as users type.&lt;/li&gt;
&lt;li&gt;Rank results by relevance, with optional popularity and recency signals.&lt;/li&gt;
&lt;li&gt;Support filters (brand, category, price, availability) and facets (counts by brand or category).&lt;/li&gt;
&lt;li&gt;Handle pagination and, where useful, typo tolerance.&lt;/li&gt;
&lt;li&gt;Reflect product additions, edits, and deletions within an agreed freshness window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suggestions can be &lt;strong&gt;query completions&lt;/strong&gt; (&lt;code&gt;iphone 15 case&lt;/code&gt;), &lt;strong&gt;entity suggestions&lt;/strong&gt; (a particular product), or a mixture. This decision affects indexing and ranking; a trie of words is not automatically a complete autocomplete product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-functional requirements
&lt;/h3&gt;

&lt;p&gt;For discussion, assume:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Illustrative target or assumption&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog&lt;/td&gt;
&lt;td&gt;10 million searchable products&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full search&lt;/td&gt;
&lt;td&gt;2,000 requests/second at peak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autocomplete&lt;/td&gt;
&lt;td&gt;10,000 requests/second at peak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-search latency&lt;/td&gt;
&lt;td&gt;P99 below 200 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autocomplete latency&lt;/td&gt;
&lt;td&gt;P99 below 100 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index freshness&lt;/td&gt;
&lt;td&gt;Most updates visible within several seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Continue serving when individual nodes fail, subject to product correctness rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;These are design assumptions, not universal search-industry benchmarks.&lt;/strong&gt; The actual requirements depend on corpus size, languages, query mix, hardware, and product expectations.&lt;/p&gt;

&lt;p&gt;A crucial distinction: &lt;strong&gt;search correctness, ranking quality, freshness, and availability are different properties&lt;/strong&gt;. A query can return valid matches in the wrong order; an index can be available but stale; a partial result can be fast but incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why Not Just Use SQL &lt;code&gt;LIKE&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;The simplest implementation queries the transactional database directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%iphone%'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A leading-wildcard pattern such as &lt;code&gt;%iphone%&lt;/code&gt; generally cannot use a conventional B-tree index as an efficient prefix range lookup. On a large table, it may require expensive scanning.&lt;/p&gt;

&lt;p&gt;But the correct conclusion is &lt;strong&gt;not&lt;/strong&gt; that relational databases cannot do full-text search. PostgreSQL, for example, supports full-text indexes and trigram-based approaches. Database-native search may be the right choice for a smaller catalog or modest query volume.&lt;/p&gt;

&lt;p&gt;A dedicated search engine becomes attractive when we need several capabilities together: inverted indexes, language-aware analysis, flexible ranking, fuzzy matching, faceting, and independent scaling of read-heavy search traffic. The trade-off is another data system and an eventually consistent copy of source data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; keep the transactional database authoritative and maintain a separate, query-optimized search index for this example.&lt;/p&gt;

&lt;p&gt;For a deeper comparison, see &lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/tradeoffs-decisions" rel="noopener noreferrer"&gt;Database vs Dedicated Search Index&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. High-Level Search &amp;amp; Autocomplete Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                        WRITE / INDEXING PATH

  Product Service
        |
        v
  Source Database  (source of truth)
        |
        v
  CDC / Reliable Change Events
        |
        v
  Durable Stream / Queue
        |
        v
  Indexing Workers ----&amp;gt; Transform + Analyze + Version Check
        |                              |
        +------------------------------+
        |                              |
        v                              v
  Full-Text Search Index       Autocomplete Suggestion Index
  (inverted index)            (prefix / n-gram / precomputed)

                        READ / QUERY PATH

  Browser / App
        |
        v
  Search API / Gateway
        |
        +---- /suggest ---&amp;gt; Suggestion Cache ---&amp;gt; Suggestion Service
        |                                             |
        |                                             v
        |                                      Prefix Candidates
        |                                             |
        |                                      Suggestion Ranking
        |
        +---- /search ----&amp;gt; Result Cache ----&amp;gt; Search Coordinator
                                                  |
                                      +-----------+-----------+
                                      |           |           |
                                   Shard A     Shard B     Shard C
                                      |           |           |
                                      +-----------+-----------+
                                                  |
                                          Merge / Rank Top-K
                                                  |
                                             Search Results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram separates &lt;strong&gt;indexing&lt;/strong&gt; from &lt;strong&gt;serving&lt;/strong&gt;. The search engine does not need to query the source database for every request. That separation protects transactional workloads and allows search capacity to scale independently.&lt;/p&gt;

&lt;p&gt;The diagram also shows a dedicated suggestion index. This is a &lt;strong&gt;choice for our assumed low-latency, high-autocomplete-QPS workload&lt;/strong&gt;, not a requirement for every implementation. A unified search engine with prefix queries can be simpler and entirely sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. How an Inverted Index Works
&lt;/h2&gt;

&lt;p&gt;A full-text search engine typically uses an &lt;strong&gt;inverted index&lt;/strong&gt;, mapping terms to the documents containing them.&lt;/p&gt;

&lt;p&gt;Suppose the catalog contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;D1: "Wireless iPhone case"
D2: "iPhone fast charger"
D3: "Wireless charging pad"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After suitable text analysis, a simplified index is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"iphone"   -&amp;gt; [D1, D2]
"wireless" -&amp;gt; [D1, D3]
"case"     -&amp;gt; [D1]
"charger"  -&amp;gt; [D2]
"charging" -&amp;gt; [D3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query for &lt;code&gt;wireless iphone&lt;/code&gt; can retrieve candidate documents by intersecting or combining the posting lists, depending on AND/OR query semantics. The search engine then scores the candidates.&lt;/p&gt;

&lt;p&gt;Real &lt;strong&gt;posting lists&lt;/strong&gt; may also store term frequencies and positions. Positions allow phrase queries to distinguish &lt;code&gt;wireless iphone case&lt;/code&gt; from the same words appearing far apart.&lt;/p&gt;

&lt;p&gt;Term lookup can be fast, but &lt;strong&gt;processing the posting list is not constant-time regardless of corpus size&lt;/strong&gt;. A common term can appear in millions of documents. Compression, skip structures, and top-k pruning help control that cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Text analysis must be compatible
&lt;/h3&gt;

&lt;p&gt;Indexing and querying must use compatible analyzers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw text -&amp;gt; Tokenize -&amp;gt; Normalize -&amp;gt; Optional stemming/synonyms -&amp;gt; Terms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalization:&lt;/strong&gt; case folding and, when appropriate, accent handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokenization:&lt;/strong&gt; splitting text into meaningful units; language-dependent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stemming/lemmatization:&lt;/strong&gt; optional normalization of related word forms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synonyms:&lt;/strong&gt; domain-specific mappings such as &lt;code&gt;tv&lt;/code&gt; and &lt;code&gt;television&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field-specific behavior:&lt;/strong&gt; product titles may be analyzed; SKUs and IDs usually require exact matching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applying aggressive stemming to an SKU or brand field can reduce correctness rather than improve it. Likewise, changing an analyzer can require &lt;strong&gt;reindexing existing documents&lt;/strong&gt;, because the stored terms were generated under the previous rules.&lt;/p&gt;

&lt;p&gt;For the internals, see &lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/architecture-deep-dive" rel="noopener noreferrer"&gt;Search &amp;amp; Autocomplete — Architecture Deep Dive&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Autocomplete System Design: Generating Suggestions
&lt;/h2&gt;

&lt;p&gt;Autocomplete is a different workload from full search. A user may issue several suggestion requests before submitting one search, and the usefulness of a response drops quickly as they type the next character.&lt;/p&gt;

&lt;p&gt;The serving pipeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Typed prefix -&amp;gt; Candidate generation -&amp;gt; Suggestion ranking -&amp;gt; Top suggestions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several legitimate candidate-generation approaches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Main trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search-engine prefix query&lt;/td&gt;
&lt;td&gt;Reuses existing search infrastructure&lt;/td&gt;
&lt;td&gt;Shares resources with full search; performance depends on engine and load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge n-grams&lt;/td&gt;
&lt;td&gt;Prefixes are indexed as terms for fast lookup&lt;/td&gt;
&lt;td&gt;More index storage and write amplification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trie / FST-like structure&lt;/td&gt;
&lt;td&gt;Purpose-built prefix traversal; can precompute top suggestions&lt;/td&gt;
&lt;td&gt;Additional update, memory, and operational complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precomputed query-log suggestions&lt;/td&gt;
&lt;td&gt;Efficient for frequently searched queries&lt;/td&gt;
&lt;td&gt;Requires freshness, spam filtering, and safe handling of query logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Trie example
&lt;/h3&gt;

&lt;p&gt;For the terms &lt;code&gt;car&lt;/code&gt;, &lt;code&gt;card&lt;/code&gt;, and &lt;code&gt;care&lt;/code&gt;, a conceptual trie looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(root)
   |
   c
   |
   a
   |
   r  ("car")
  / \
 d   e
("card") ("care")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking up &lt;code&gt;car&lt;/code&gt; finds the node for that prefix. Retrieving the best suggestions can still require traversing many descendants &lt;strong&gt;unless the structure maintains precomputed top-k suggestions at nodes&lt;/strong&gt;. Prefix lookup and ranking are separate operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge n-grams example
&lt;/h3&gt;

&lt;p&gt;For &lt;code&gt;iphone&lt;/code&gt;, index prefixes such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ip, iph, ipho, iphon, iphone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a query for &lt;code&gt;iph&lt;/code&gt; can match the corresponding prefix term. In practice, minimum and maximum prefix lengths should be configured deliberately: indexing every single-character prefix may be wasteful, and very short prefixes often produce noisy suggestions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ranking suggestions
&lt;/h3&gt;

&lt;p&gt;Prefix matching only produces candidates. Suggestions can then be ordered by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historical search or selection frequency.&lt;/li&gt;
&lt;li&gt;Recent trending activity.&lt;/li&gt;
&lt;li&gt;Locale, language, and category context.&lt;/li&gt;
&lt;li&gt;Optional personalization.&lt;/li&gt;
&lt;li&gt;Safety, policy, and abuse controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Never treat raw query-log popularity as automatically trustworthy.&lt;/strong&gt; Bots, spam, inappropriate queries, and old trends can distort the list. Query logs may also contain sensitive user input, so aggregation and retention need appropriate privacy controls.&lt;/p&gt;

&lt;p&gt;For this example, a dedicated suggestion index helps isolate autocomplete latency from expensive full-text searches. It remains a workload-driven decision, not a universally superior architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Preventing Autocomplete Request Storms
&lt;/h2&gt;

&lt;p&gt;Without client-side controls, a user typing &lt;code&gt;iphone&lt;/code&gt; could issue six requests in quick succession. Across many active users, this is a meaningful traffic multiplier.&lt;/p&gt;

&lt;p&gt;Use a combination of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Debouncing:&lt;/strong&gt; wait a short, tuned interval after typing before requesting suggestions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum prefix length:&lt;/strong&gt; avoid expensive, low-value one-character requests where the product allows it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancellation or stale-response suppression:&lt;/strong&gt; prevent a slow response for &lt;code&gt;ip&lt;/code&gt; from replacing newer results for &lt;code&gt;iphone&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side rate limits:&lt;/strong&gt; protect shared resources from abusive clients or bursts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching hot prefixes:&lt;/strong&gt; popular prefixes often have excellent reuse.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cancellation is an optimization, not a correctness guarantee: a request may already have reached the server. The client should still ignore responses that do not correspond to its latest input.&lt;/p&gt;

&lt;p&gt;A practical API might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /suggest?q=iph&amp;amp;locale=en-US&amp;amp;limit=8
GET /search?q=iphone%20case&amp;amp;category=accessories&amp;amp;sort=relevance&amp;amp;limit=20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service should bound input length, expansion complexity, and returned suggestion count. The cache key must include any locale, market, or other context that changes the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Search Ranking: Retrieval Is Not Relevance
&lt;/h2&gt;

&lt;p&gt;An inverted index answers &lt;strong&gt;which documents might match&lt;/strong&gt;. It does not, by itself, determine which matches are most useful.&lt;/p&gt;

&lt;p&gt;A practical ranking pipeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query
  |
  v
Candidate retrieval + eligibility filters
  |
  v
First-stage lexical ranking (for example, BM25)
  |
  v
Optional re-ranking of a bounded top-N set
  |
  v
Final eligibility / business rules + top-K results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt; is a common lexical baseline. It rewards matches on distinctive terms, considers term frequency with diminishing returns, and normalizes for document length. It is more nuanced than simply counting keyword occurrences.&lt;/p&gt;

&lt;p&gt;An optional second-stage ranker can incorporate popularity, recency, category relevance, or personalization. Running an expensive model on a bounded candidate set controls &lt;strong&gt;re-ranking cost&lt;/strong&gt;, not the cost of retrieving candidates from the index.&lt;/p&gt;

&lt;p&gt;Business rules need care. If an out-of-stock product must never be shown, filtering it &lt;strong&gt;only after selecting the top 10&lt;/strong&gt; could leave too few results even when valid products exist lower down. Apply hard eligibility constraints early enough to preserve the requested result count, or retrieve enough additional candidates to satisfy them.&lt;/p&gt;

&lt;p&gt;When the optional re-ranker fails, the system can fall back to lexical ranking, preserving search availability with reduced relevance quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Filters, Facets, and Typo Tolerance
&lt;/h2&gt;

&lt;p&gt;These features are related but not interchangeable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filters&lt;/strong&gt; constrain eligibility: &lt;code&gt;brand = Apple&lt;/code&gt;, &lt;code&gt;price &amp;lt;= 500&lt;/code&gt;, or &lt;code&gt;in_stock = true&lt;/code&gt;. They generally should not change textual relevance scores unless that is a deliberate ranking rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Facets&lt;/strong&gt; aggregate the matching set: for example, how many eligible products fall into each brand or price range. Facets may be expensive on large result sets or high-cardinality fields. Their semantics must be defined carefully, especially whether a facet's own filter is included when computing its counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typo tolerance&lt;/strong&gt; expands potential matches. Common techniques include edit-distance matching, n-grams, and spelling correction. They can improve recall, but broad fuzzy matching on a two-character query may expand into an enormous set of candidate terms.&lt;/p&gt;

&lt;p&gt;Use bounded edit distance, minimum query length, and query-complexity limits. Exact-match identifiers such as SKUs should generally not receive the same fuzzy treatment as natural-language titles.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Keeping the Search Index Up to Date
&lt;/h2&gt;

&lt;p&gt;The source database is authoritative; the search index is a &lt;strong&gt;derived view&lt;/strong&gt;. Product updates need to reach the index reliably.&lt;/p&gt;

&lt;p&gt;A typical pipeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Committed DB change
        |
        v
CDC or reliably published event
        |
        v
Durable log / queue
        |
        v
Indexer: validate -&amp;gt; transform -&amp;gt; version-check
        |
        v
Search index (+ suggestion index when applicable)
        |
        v
Refresh / visibility to queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CDC is one option, not the only one. Application-emitted events and scheduled imports can also work. If an application writes the database and separately publishes an event, it must address the &lt;strong&gt;dual-write failure gap&lt;/strong&gt; (for example, through a transactional outbox).&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate and out-of-order updates
&lt;/h3&gt;

&lt;p&gt;Consider the same product receiving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update v11: price = 120
Update v12: price = 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If v12 is applied first and a delayed v11 is then applied without checking versions, search shows the wrong price. The indexer must reject stale updates based on a trustworthy, monotonically increasing &lt;strong&gt;per-document version&lt;/strong&gt;. A wall-clock &lt;code&gt;updated_at&lt;/code&gt; timestamp alone may not be reliable enough if timestamps collide or clocks are inconsistent.&lt;/p&gt;

&lt;p&gt;Deletes require the same care. A delayed pre-delete update must not resurrect a deleted product. The system needs version-aware tombstone handling or an equivalent mechanism that prevents stale resurrection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Indexing lag is not just queue lag
&lt;/h3&gt;

&lt;p&gt;Even after an indexer successfully writes a document, the search engine may not expose it to normal queries until a refresh. Observed freshness includes both &lt;strong&gt;pipeline delay&lt;/strong&gt; and &lt;strong&gt;index visibility delay&lt;/strong&gt;, plus any applicable result-cache staleness.&lt;/p&gt;

&lt;p&gt;Reducing refresh intervals can improve freshness but increases indexing/segment-management work. A freshness target should therefore be an explicit product requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full reindexing without taking search offline
&lt;/h3&gt;

&lt;p&gt;Changing analyzers, mappings, or transformations can require rebuilding the corpus. A safer approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new versioned index.&lt;/li&gt;
&lt;li&gt;Bulk-load a consistent source snapshot.&lt;/li&gt;
&lt;li&gt;Apply and catch up on changes made during the rebuild.&lt;/li&gt;
&lt;li&gt;Validate counts, sample documents, deletes, and search quality.&lt;/li&gt;
&lt;li&gt;Switch the serving alias or routing layer to the new index.&lt;/li&gt;
&lt;li&gt;Retain the old index briefly for rollback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The snapshot and change stream need a well-defined handoff position; otherwise updates made during the rebuild can be missed. Do not destructively rebuild the only live index and assume rollback will be easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Caching Search Results Correctly
&lt;/h2&gt;

&lt;p&gt;Popular queries repeat, making caching attractive. But a cache key based only on the raw query string is often &lt;strong&gt;incorrect&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two requests for &lt;code&gt;iphone&lt;/code&gt; can produce different responses depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filters and sort order.&lt;/li&gt;
&lt;li&gt;Locale, region, or tenant.&lt;/li&gt;
&lt;li&gt;Pagination cursor or page.&lt;/li&gt;
&lt;li&gt;Ranking/index version.&lt;/li&gt;
&lt;li&gt;Personalization or user-specific eligibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cache only across requests that are genuinely allowed to share the same response. Authorization or tenant boundaries must never be lost through cache-key normalization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cache key = normalized query + filters + sort + locale
          + page/cursor + index/ranker version
          + relevant access/personalization context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose TTLs according to query reuse and freshness requirements; there is no universally correct &lt;code&gt;60-second&lt;/code&gt; TTL. For personalized queries with little full-response reuse, caching candidates or reusable components may work better.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache stampede
&lt;/h3&gt;

&lt;p&gt;Suppose the cache absorbs 80% of incoming search requests. The index normally receives 20%. If the cache disappears and incoming demand stays the same, index-facing traffic can rise by approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 / (1 - 0.80) = 5x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is &lt;strong&gt;an example derived from an assumed hit rate&lt;/strong&gt;, not a universal multiplier. Request coalescing, TTL jitter, stale-while-revalidate where acceptable, capacity headroom, and load shedding can limit the damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Sharding, Replication, and Scatter-Gather
&lt;/h2&gt;

&lt;p&gt;When one search node cannot efficiently hold or serve the corpus, distribute documents across &lt;strong&gt;shards&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hash-based document sharding
&lt;/h3&gt;

&lt;p&gt;A document ID hash can spread documents relatively evenly. A broad keyword query may then need to reach every relevant shard, because matching documents could be anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routed sharding
&lt;/h3&gt;

&lt;p&gt;Routing by tenant, region, or category can reduce query fan-out &lt;strong&gt;when the query specifies that routing dimension&lt;/strong&gt;. It can also create skew: one very large tenant may produce a hot shard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scatter-gather query execution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Search Coordinator
             /       |        \
          Shard A  Shard B   Shard C
          top-K     top-K     top-K
             \       |        /
              Merge + Global Top-K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each shard produces its best local candidates; the coordinator merges them. For a straightforward global top-K under a consistent comparable scoring function, retrieving &lt;strong&gt;K from each shard is sufficient&lt;/strong&gt; before global merging. Additional candidates may be required by a subsequent global re-ranking stage, pagination strategy, or other engine-specific behavior; oversampling is not inherently necessary just because results are sharded.&lt;/p&gt;

&lt;p&gt;Distributed search is sensitive to &lt;strong&gt;tail latency&lt;/strong&gt;: a single slow shard can delay the response. Replicas improve availability and read capacity, but do not guarantee uninterrupted or perfectly fresh service. If no replica of a required shard responds, the system must explicitly choose to fail the query or return &lt;strong&gt;clearly marked partial results&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a product catalog, partial results may be acceptable. For legal discovery or financial reporting, silently omitting a shard's results could be unacceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Pagination: Why Deep OFFSET Becomes Expensive
&lt;/h2&gt;

&lt;p&gt;Simple pagination is easy to explain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Conceptual syntax; actual search-engine APIs differ.&lt;/span&gt;
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a distributed search, deep offset pagination may require shards to collect many more results than the 20 shown to the user, increasing work with page depth. Changes between requests can also cause duplicates or omissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search-after/cursor pagination&lt;/strong&gt; carries the sort position of the last result, including a deterministic tie-breaker such as document ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iphone case"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"search_after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"product_991823"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example assumes a score-descending, ID-tiebroken sort; the actual cursor and comparison rules must match the engine's configured ordering. A cursor avoids repeatedly skipping earlier results, but it does &lt;strong&gt;not&lt;/strong&gt; freeze the underlying result set. If stable multi-page results matter, combine it with point-in-time/snapshot semantics supported by the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Production Failure Scenarios
&lt;/h2&gt;

&lt;p&gt;A design is incomplete if it only describes the happy path. These are the failures most likely to change our architectural choices.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;User-visible impact&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search shard unavailable&lt;/td&gt;
&lt;td&gt;Errors, higher latency, or incomplete results&lt;/td&gt;
&lt;td&gt;Route to healthy replica; otherwise follow explicit fail/partial policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache outage&lt;/td&gt;
&lt;td&gt;Search cluster receives sudden extra traffic&lt;/td&gt;
&lt;td&gt;Request coalescing, headroom, load shedding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Indexer backlog&lt;/td&gt;
&lt;td&gt;Newly changed products remain stale in search&lt;/td&gt;
&lt;td&gt;Monitor oldest event age, retain backlog, scale catch-up capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost CDC events / retention gap&lt;/td&gt;
&lt;td&gt;Index may permanently diverge&lt;/td&gt;
&lt;td&gt;Reconcile against source; rebuild when replay is impossible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad analyzer/index deployment&lt;/td&gt;
&lt;td&gt;Wrong matches or degraded relevance despite healthy servers&lt;/td&gt;
&lt;td&gt;Versioned indexes, validation, alias rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old update arrives after delete&lt;/td&gt;
&lt;td&gt;Deleted product reappears&lt;/td&gt;
&lt;td&gt;Source-version checks and durable tombstone semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot prefix or expensive fuzzy query&lt;/td&gt;
&lt;td&gt;CPU spikes, increased tail latency&lt;/td&gt;
&lt;td&gt;Cache hot prefixes; bound expansions, deadlines, and query complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ranking dependency fails&lt;/td&gt;
&lt;td&gt;Search relevance degrades&lt;/td&gt;
&lt;td&gt;Fall back to first-stage lexical ranking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autocomplete index lags&lt;/td&gt;
&lt;td&gt;Search finds a product that suggestions omit&lt;/td&gt;
&lt;td&gt;Separate freshness monitoring and defined suggestion-update cadence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Monitor more than availability
&lt;/h3&gt;

&lt;p&gt;A search service can return HTTP 200 while its results are wrong. Useful measurements include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search and autocomplete P50/P95/P99 latency, error rate, and QPS.&lt;/li&gt;
&lt;li&gt;Per-shard latency, timeouts, load, and replica health.&lt;/li&gt;
&lt;li&gt;Cache hit rate, hot-key pressure, and upstream request coalescing.&lt;/li&gt;
&lt;li&gt;Indexing backlog, &lt;strong&gt;oldest unprocessed event age&lt;/strong&gt;, and end-to-end freshness.&lt;/li&gt;
&lt;li&gt;Source/index divergence checks, delete correctness, and failed indexing events.&lt;/li&gt;
&lt;li&gt;Zero-result rate, relevance regression tests, and other quality signals interpreted in context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more failure walkthroughs, see &lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/failure-scale-scenarios" rel="noopener noreferrer"&gt;Search &amp;amp; Autocomplete — Failure &amp;amp; Scale Scenarios&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Major Architecture Trade-offs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Simpler option&lt;/th&gt;
&lt;th&gt;When added complexity may be justified&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search engine&lt;/td&gt;
&lt;td&gt;Database-native full-text search&lt;/td&gt;
&lt;td&gt;High QPS, sophisticated ranking/facets, independent scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autocomplete&lt;/td&gt;
&lt;td&gt;Search-engine prefix queries&lt;/td&gt;
&lt;td&gt;Dedicated suggestion index for latency isolation or specialized ranking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix representation&lt;/td&gt;
&lt;td&gt;Edge n-grams&lt;/td&gt;
&lt;td&gt;Trie/FST or precomputed suggestions for specific memory/update/latency goals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change propagation&lt;/td&gt;
&lt;td&gt;Batch import or polling&lt;/td&gt;
&lt;td&gt;CDC or reliable domain events for tighter freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ranking&lt;/td&gt;
&lt;td&gt;Lexical relevance only&lt;/td&gt;
&lt;td&gt;Re-rank bounded candidates for richer relevance signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache&lt;/td&gt;
&lt;td&gt;Cache common final responses&lt;/td&gt;
&lt;td&gt;Cache candidates/components when full responses have low reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharding&lt;/td&gt;
&lt;td&gt;Hash by document ID&lt;/td&gt;
&lt;td&gt;Routing by tenant/domain when queries can exploit it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagination&lt;/td&gt;
&lt;td&gt;Shallow offset&lt;/td&gt;
&lt;td&gt;Search-after for deep pages; snapshot for stable multi-page views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shard failure&lt;/td&gt;
&lt;td&gt;Fail the query&lt;/td&gt;
&lt;td&gt;Marked partial results when the product tolerates incompleteness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The correct design is the &lt;strong&gt;simplest architecture that meets the stated requirements&lt;/strong&gt;. A trie, CDC pipeline, separate ranking service, and multiple caching layers should each have a reason to exist. Adding all of them by default is not a sign of a stronger design.&lt;/p&gt;

&lt;p&gt;See the full &lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions&lt;/a&gt; discussion for the alternatives behind these choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Search &amp;amp; Autocomplete System Design Interview Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not use the primary database for search?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For small workloads, we may. At higher scale, a dedicated index provides search-specific structures and independent scaling. Avoid claiming SQL databases cannot perform full-text search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between an inverted index and a trie?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An inverted index maps terms to matching documents. A trie organizes prefixes for efficient completion lookup. They answer different questions, and autocomplete does not necessarily require a trie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle typos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use bounded fuzzy matching, n-grams, or spelling correction where appropriate. Keep exact identifiers exact and prevent broad short-prefix expansions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does a new product become searchable?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A committed source change reaches an indexer through CDC or reliable events, is applied with version checks, and becomes visible after the search engine's refresh. The process is usually eventually consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if indexing stops for 30 minutes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search can continue serving stale results. Recovery requires the missing events to remain available and the indexer to catch up faster than new events arrive. If event history is lost, reconciliation or reindexing is necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use BM25 and then a re-ranker?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;BM25 gives an efficient lexical baseline. A bounded re-ranking stage can use richer signals without scoring the entire corpus with an expensive model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you prevent autocomplete from overwhelming the backend?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Debounce, ignore stale responses, enforce minimum prefix lengths where suitable, cache common prefixes, and apply server-side rate and complexity limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when a shard is down?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Try a healthy replica. If no copy is available, either fail the query or return results explicitly marked as partial, depending on whether incompleteness is acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you change analyzers without downtime?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Build a new index from a snapshot, replay intervening changes, validate, switch the serving alias, and retain the old version for rollback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not use OFFSET for page 500?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deep offset work grows with depth and results can shift between requests. Search-after avoids repeated skipping; point-in-time semantics can provide a stable view when required.&lt;/p&gt;

&lt;p&gt;For a broader question bank, see &lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/interview-perspective" rel="noopener noreferrer"&gt;Search &amp;amp; Autocomplete — Interview Perspective&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaways
&lt;/h2&gt;

&lt;p&gt;A reliable search system separates &lt;strong&gt;the source of truth from the search-optimized view&lt;/strong&gt;, and separates &lt;strong&gt;candidate retrieval from ranking&lt;/strong&gt;. Autocomplete introduces another latency-sensitive workload whose candidate generation, ranking, and request controls deserve explicit design decisions.&lt;/p&gt;

&lt;p&gt;The most important production details are easy to overlook: compatible text analyzers, version-aware indexing, safe delete handling, refresh lag, correct cache keys, bounded expensive queries, shard failure semantics, and rebuilds that do not destroy the live index.&lt;/p&gt;

&lt;p&gt;None of these mechanisms is a universal prescription. Start with the actual search experience, traffic, freshness, and correctness requirements; introduce complexity only where those requirements justify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/complete-design" rel="noopener noreferrer"&gt;Search &amp;amp; Autocomplete — Complete Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/architecture-deep-dive" rel="noopener noreferrer"&gt;Search &amp;amp; Autocomplete — Architecture Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/tradeoffs-decisions" rel="noopener noreferrer"&gt;Search &amp;amp; Autocomplete — Trade-offs &amp;amp; Decisions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/failure-scale-scenarios" rel="noopener noreferrer"&gt;Search &amp;amp; Autocomplete — Failure &amp;amp; Scale Scenarios&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/system-design/foundations/search-autocomplete/interview-perspective" rel="noopener noreferrer"&gt;Search &amp;amp; Autocomplete — Interview Perspective&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seeitflow.com/" rel="noopener noreferrer"&gt;SeeItFlow — Visual Software Engineering Learning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>backend</category>
      <category>search</category>
    </item>
    <item>
      <title>Designing a Notification System: Queues, Retries, Rate Limiting &amp; Scale</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:44:37 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/designing-a-notification-system-queues-retries-rate-limiting-scale-4bg1</link>
      <guid>https://dev.to/mangeshmandlik/designing-a-notification-system-queues-retries-rate-limiting-scale-4bg1</guid>
      <description>&lt;p&gt;&lt;em&gt;The difficult part isn't sending an email. It's making sure a slow provider, a viral campaign, or a retry storm cannot take down the services that need to send one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffexlso17cnv15h2dw7oh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffexlso17cnv15h2dw7oh.png" alt="complete architecture" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagine an e-commerce platform just completed an order. The order service needs to send a confirmation email, the payment service needs to send a receipt, and a security service may need to send an urgent login alert.&lt;/p&gt;

&lt;p&gt;Now imagine the email provider starts taking eight seconds to respond.&lt;/p&gt;

&lt;p&gt;If those services call the provider synchronously, an external notification dependency suddenly becomes part of the checkout and login latency budget. If a promotional campaign simultaneously produces millions of messages, critical security alerts can end up behind a mountain of marketing traffic.&lt;/p&gt;

&lt;p&gt;A production notification system has to solve a more interesting problem than sending messages: &lt;strong&gt;accept notification intent durably, then deliver it independently at the rate each downstream provider can sustain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's build that architecture from first principles, and examine where seemingly sensible implementations fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements: What Are We Designing?
&lt;/h2&gt;

&lt;p&gt;Our service accepts requests from many producer applications and delivers notifications through &lt;strong&gt;push, email, and SMS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It should support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional messages such as order confirmations and payment updates.&lt;/li&gt;
&lt;li&gt;Time-sensitive messages such as security alerts and one-time passcodes.&lt;/li&gt;
&lt;li&gt;Promotional campaigns, potentially targeting millions of recipients.&lt;/li&gt;
&lt;li&gt;Recipient preferences, opt-outs, quiet hours, and valid destination checks.&lt;/li&gt;
&lt;li&gt;Delivery status, retries, and investigation of failed messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important non-functional requirements are &lt;strong&gt;fast producer acknowledgment, durable processing, channel isolation, preferential latency for urgent traffic, and observability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One distinction defines our API contract:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ACCEPTED does not mean DELIVERED.&lt;/strong&gt; It means the system has durably recorded the notification for asynchronous processing. Delivery may later succeed, fail permanently, expire, or be suppressed by policy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That contract is the foundation of the &lt;a href="https://seeitflow.com/system-design/foundations/notification/complete-design" rel="noopener noreferrer"&gt;Notification System Complete Design&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Capacity Estimation: Ingestion and Delivery Are Different Rates
&lt;/h2&gt;

&lt;p&gt;Consider an illustrative traffic burst:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Example workload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Incoming notifications&lt;/td&gt;
&lt;td&gt;5,000/second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider's sustainable send capacity&lt;/td&gt;
&lt;td&gt;800/second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Excess arriving during the burst&lt;/td&gt;
&lt;td&gt;4,200/second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backlog after a 60-second burst&lt;/td&gt;
&lt;td&gt;252,000 notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If incoming traffic drops to zero and the provider continues delivering at 800/second, that backlog alone takes &lt;strong&gt;315 seconds&lt;/strong&gt;, or about &lt;strong&gt;5 minutes 15 seconds&lt;/strong&gt;, to drain.&lt;/p&gt;

&lt;p&gt;In reality, new traffic may continue arriving, reducing the spare capacity available to clear old messages.&lt;/p&gt;

&lt;p&gt;This leads to a rule that is easy to overlook:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A queue absorbs a temporary difference between arrival and delivery rates. It does not manufacture provider capacity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If 5,000 messages arrive every second indefinitely and the provider can only deliver 800, the backlog grows indefinitely. We eventually need more provider capacity, slower campaign ingestion, traffic shedding, or an explicitly longer delivery window for less urgent notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Start With the Simplest Architecture — and Watch It Break
&lt;/h2&gt;

&lt;p&gt;The first implementation might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Service
     |
     v
Notification API
     |
     v
Email / SMS / Push Provider
     |
     v
Response to Order Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5y89ee2svljq48e7ys4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5y89ee2svljq48e7ys4.png" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's simple, but the calling service is now waiting for a provider it does not control.&lt;/p&gt;

&lt;p&gt;If the provider slows to an eight-second p99 response time, the producer's request may inherit that delay. If the provider is unavailable, notifications may fail alongside otherwise healthy business operations.&lt;/p&gt;

&lt;p&gt;The first meaningful architectural change is not adding ten microservices. It's &lt;strong&gt;putting a durable asynchronous boundary between notification creation and delivery&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer
   |
   v
Notification API
   |
   v
Durable Queue  -----&amp;gt;  Delivery Workers  -----&amp;gt;  Provider
   ^                         |
   |                         v
Fast acceptance         Retry / failure handling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz156fwmisaekuk2hy9wd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz156fwmisaekuk2hy9wd.png" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the producer does not wait for the provider. Workers can process messages at a controlled pace, and a temporary provider outage becomes a backlog rather than an immediate failure of every calling application.&lt;/p&gt;

&lt;p&gt;But the phrase “durable queue” hides a subtle consistency problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Durable Ingestion: The Database–Queue Dual-Write Problem
&lt;/h2&gt;

&lt;p&gt;A typical notification API must record the notification's state in a database and publish work to a message broker.&lt;/p&gt;

&lt;p&gt;Suppose we implement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert notification into the database.&lt;/li&gt;
&lt;li&gt;Publish the notification to Kafka, SQS, or RabbitMQ.&lt;/li&gt;
&lt;li&gt;Return success.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What if step 1 succeeds but step 2 fails?&lt;/p&gt;

&lt;p&gt;The database says the notification exists, but no worker ever receives it.&lt;/p&gt;

&lt;p&gt;Reversing the order doesn't solve it. The broker may accept the message while the database write fails, leaving workers processing a notification with no durable tracking record.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transactional outbox
&lt;/h3&gt;

&lt;p&gt;A practical solution is to write &lt;strong&gt;the notification record and an outbox event in the same database transaction&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Notification API
       |
       v
Single DB transaction
  +------------------------+
  | Notification record    |
  | Outbox event           |
  +------------------------+
       |
       v
Outbox Publisher  ---&amp;gt;  Message Broker  ---&amp;gt;  Workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API can acknowledge once that durable transaction commits, while the outbox publisher continues retrying publication if the broker is temporarily unavailable.&lt;/p&gt;

&lt;p&gt;This closes the dual-write gap, but &lt;strong&gt;does not guarantee exactly-once processing&lt;/strong&gt;. A publisher can send an event successfully and crash before marking it published, causing it to publish again after restart. Consumers still need idempotent handling.&lt;/p&gt;

&lt;p&gt;The exact failure windows and guarantees are covered in &lt;a href="https://seeitflow.com/system-design/foundations/notification/architecture-deep-dive" rel="noopener noreferrer"&gt;Notification System Architecture Deep Dive — Durable Ingestion&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. API Design: Accept Intent, Not Delivery Promises
&lt;/h2&gt;

&lt;p&gt;A minimal API might expose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /notifications
Content-Type: application/json
Idempotency-Key: order-8421-confirmation
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"normal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"template_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-confirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"template_variables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8421"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful response returns a notification ID and &lt;code&gt;ACCEPTED&lt;/code&gt; status — not &lt;code&gt;DELIVERED&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notification_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notif-9271"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACCEPTED"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other useful endpoints include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /notifications/{id}/status&lt;/code&gt; — inspect the lifecycle state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /notifications/batch&lt;/code&gt; — accept a campaign definition and return a batch ID without synchronously expanding millions of recipients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The producer-supplied idempotency key matters. If the producer times out after the API successfully commits, it may retry. The same key should return the existing logical notification instead of creating another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. High-Level Architecture: Isolate Channels and Priorities
&lt;/h2&gt;

&lt;p&gt;A single queue with one pool of workers looks attractive until the email provider fails while push notifications are healthy.&lt;/p&gt;

&lt;p&gt;We want the channels to fail and scale independently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                       Producer Services
                              |
                              v
                       Notification API
                              |
                        Durable Ingestion
                              |
                              v
                   Policy / Channel Router
                              |
                  +-----------+-----------+
                  |           |           |
               Push Queue  Email Queue  SMS Queue
                  |           |           |
               Workers     Workers     Workers
                  |           |           |
             Rate Limiter Rate Limiter Rate Limiter
                  |           |           |
                FCM/APNs   Email Provider SMS Provider
                  \           |           /
                   \          |          /
                    Delivery State &amp;amp; Reconciliation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv85mk1jgesjq9rxssoek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv85mk1jgesjq9rxssoek.png" width="799" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each channel can have its own backlog, retry policy, worker concurrency, provider adapter, and rate-limit budget.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffexlso17cnv15h2dw7oh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffexlso17cnv15h2dw7oh.png" alt="complete architecture" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The architecture also needs &lt;strong&gt;priority-aware scheduling&lt;/strong&gt; within or across channel queues. We'll address that next.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Priority Queues: Security Alerts Must Not Wait Behind Campaigns
&lt;/h2&gt;

&lt;p&gt;Imagine a promotional campaign enqueues 10 million notifications. A security alert arrives one second later.&lt;/p&gt;

&lt;p&gt;If everything shares one FIFO queue, that alert could wait behind enormous amounts of lower-value traffic.&lt;/p&gt;

&lt;p&gt;Separate priority classes help:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Typical traffic&lt;/th&gt;
&lt;th&gt;Desired behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Security alerts, payment failures&lt;/td&gt;
&lt;td&gt;Preferential low latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal&lt;/td&gt;
&lt;td&gt;Order confirmations, shipping updates&lt;/td&gt;
&lt;td&gt;Predictable progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Promotions, newsletters&lt;/td&gt;
&lt;td&gt;Can tolerate delay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But a naive rule — &lt;em&gt;always drain high priority before touching normal or low&lt;/em&gt; — creates &lt;strong&gt;starvation&lt;/strong&gt;. If high-priority traffic never fully stops, lower-priority notifications may never be served.&lt;/p&gt;

&lt;p&gt;Production alternatives include &lt;strong&gt;weighted polling&lt;/strong&gt;, &lt;strong&gt;reserved capacity&lt;/strong&gt;, and &lt;strong&gt;aging&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, a scheduler might use an illustrative ratio of eight high-priority messages, two normal, and one low per round. The exact weights are workload decisions, not universal defaults.&lt;/p&gt;

&lt;p&gt;The objective is to protect urgent traffic &lt;strong&gt;while guaranteeing that every eligible class makes progress&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://seeitflow.com/system-design/foundations/notification/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions — Strict vs Fair Priority&lt;/a&gt; for the alternatives and their costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Recipient Preferences, Push Tokens, and Templates
&lt;/h2&gt;

&lt;p&gt;Before a notification reaches a provider, the router must decide whether it is &lt;strong&gt;eligible&lt;/strong&gt; to send.&lt;/p&gt;

&lt;p&gt;That means checking the recipient's category preferences, channel permissions, opt-out status, quiet hours, and whether the destination is valid.&lt;/p&gt;

&lt;p&gt;A promotional email should not be sent simply because it was queued before the user unsubscribed. For delayed messages, cached preferences may need to be checked again close to delivery time.&lt;/p&gt;

&lt;p&gt;Push adds another complication: &lt;strong&gt;users have devices, and devices have tokens&lt;/strong&gt;. One user may have several active tokens. A provider-reported invalid token should deactivate that specific destination, not trigger endless retries or disable the user's other devices.&lt;/p&gt;

&lt;p&gt;Templates also need versioning. If an order confirmation sits in a queue for an hour and the template changes, should the message use the original or the new content? Referencing a specific template version at creation time avoids silently changing in-flight notifications.&lt;/p&gt;

&lt;p&gt;These concerns are explored separately in the &lt;a href="https://seeitflow.com/system-design/foundations/notification/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt;, including recipient eligibility, token lifecycle, localization, and render-before-queue versus render-at-delivery trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Rate Limiting and Backpressure: More Workers Won't Fix a Provider Quota
&lt;/h2&gt;

&lt;p&gt;Suppose workers are capable of making 5,000 provider requests per second, but the provider account allows only 1,000.&lt;/p&gt;

&lt;p&gt;Adding workers does not raise that account limit. It may simply generate more &lt;code&gt;429 Too Many Requests&lt;/code&gt; responses.&lt;/p&gt;

&lt;p&gt;The system needs &lt;strong&gt;provider-level rate limiting shared across distributed workers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two common designs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Centralized token bucket:&lt;/strong&gt; All workers coordinate through a shared rate limiter. This provides a global quota view but adds network calls and contention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leased quota:&lt;/strong&gt; Workers receive bounded slices of the total quota and spend them locally. This reduces coordination overhead but can leave unused quota stranded with idle workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a provider returns &lt;code&gt;429&lt;/code&gt; or &lt;code&gt;Retry-After&lt;/code&gt;, the limiter should adjust rather than repeatedly sending at the same rejected rate.&lt;/p&gt;

&lt;p&gt;The queue provides &lt;strong&gt;backpressure absorption&lt;/strong&gt; while workers respect the provider's sustainable throughput. It does not remove the need for admission control when traffic stays above capacity.&lt;/p&gt;

&lt;p&gt;For a detailed comparison, see &lt;a href="https://seeitflow.com/system-design/foundations/notification/tradeoffs-decisions" rel="noopener noreferrer"&gt;Notification System Trade-offs &amp;amp; Decisions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Retries: Why Exponential Backoff Needs Jitter and Deadlines
&lt;/h2&gt;

&lt;p&gt;A provider outage often triggers a second incident caused by the notification system itself.&lt;/p&gt;

&lt;p&gt;Picture thousands of workers receiving failures at the same moment. If they all retry one second later, then two seconds later, then four seconds later, the recovering provider gets hit by synchronized waves of traffic.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;retry storm&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Exponential backoff reduces the frequency of retries. &lt;strong&gt;Jitter&lt;/strong&gt; randomizes retry timing so workers don't all return simultaneously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Without jitter:
  Workers fail together
       | 1s
       +----&amp;gt; retry spike
       | 2s
       +----&amp;gt; retry spike

With jitter:
  Workers fail together
       |  randomized delays
       +--&amp;gt; --&amp;gt; ---&amp;gt; ----&amp;gt; spread-out retries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every failure should retry:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Temporary timeout or provider 5xx&lt;/td&gt;
&lt;td&gt;Retry with backoff and jitter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider 429&lt;/td&gt;
&lt;td&gt;Honor retry guidance; lower send rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid push token&lt;/td&gt;
&lt;td&gt;Deactivate token; do not retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malformed destination&lt;/td&gt;
&lt;td&gt;Permanent failure or appropriate terminal handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum retries exhausted&lt;/td&gt;
&lt;td&gt;Move to DLQ when appropriate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notification deadline passed&lt;/td&gt;
&lt;td&gt;Expire instead of delivering stale content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Retries should be &lt;strong&gt;scheduled&lt;/strong&gt; using delayed queues or retry topics, not by keeping worker threads asleep.&lt;/p&gt;

&lt;p&gt;And every time-sensitive notification needs a deadline. A one-time passcode delivered six hours late is not a successful user experience, even if the provider eventually accepts it.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Dead-Letter Queues: Failure Must Be Visible and Recoverable
&lt;/h2&gt;

&lt;p&gt;After a bounded number of attempts, unresolved failures can be moved to a &lt;strong&gt;dead-letter queue (DLQ)&lt;/strong&gt; for diagnosis.&lt;/p&gt;

&lt;p&gt;The DLQ is not a trash can. It is an operational tool that preserves context such as the notification ID, error, attempt count, and relevant timestamps.&lt;/p&gt;

&lt;p&gt;A growing DLQ should be monitored by &lt;strong&gt;channel, provider, error category, and message age&lt;/strong&gt;, not only by one global threshold.&lt;/p&gt;

&lt;p&gt;Replaying messages requires care:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fix the underlying failure first.&lt;/li&gt;
&lt;li&gt;Re-check expiry and recipient preferences.&lt;/li&gt;
&lt;li&gt;Replay at a controlled pace.&lt;/li&gt;
&lt;li&gt;Retain prior attempt history and observe whether the problem recurs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Dumping the entire DLQ back into the main queue can recreate the original overload.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://seeitflow.com/system-design/foundations/notification/failure-scale-scenarios" rel="noopener noreferrer"&gt;Failure &amp;amp; Scale Scenarios — DLQ Growth and Backlog Recovery&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Idempotency: Can We Guarantee Exactly-Once Notifications?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No — not universally across an external notification provider.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are three different boundaries to protect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Producer → API:&lt;/strong&gt; A retried API call should not create a second logical notification. Use a producer idempotency key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue → Worker:&lt;/strong&gt; Brokers may redeliver messages. Make worker state transitions idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker → Provider:&lt;/strong&gt; The external side effect can become ambiguous after a timeout or crash.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Worker sends notification to provider
                  |
                  v
Provider accepts request
                  |
                  v
Worker crashes before recording success
                  |
                  v
Broker redelivers the message
                  |
                  v
New worker cannot tell whether it was sent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the new worker retries, the user might receive the notification twice. If it doesn't, the notification might never arrive.&lt;/p&gt;

&lt;p&gt;A pre-send deduplication check cannot resolve this ambiguity because the successful external action was never recorded.&lt;/p&gt;

&lt;p&gt;Where supported, &lt;strong&gt;provider-side idempotency keys&lt;/strong&gt; reduce the risk. Persisting provider message IDs and reconciling against callbacks or status APIs also helps.&lt;/p&gt;

&lt;p&gt;The honest contract is &lt;strong&gt;durable at-least-once processing with strong duplicate reduction&lt;/strong&gt;, not an unsupported promise of exactly-once end-user delivery.&lt;/p&gt;

&lt;p&gt;The full crash-window analysis is in &lt;a href="https://seeitflow.com/system-design/foundations/notification/architecture-deep-dive" rel="noopener noreferrer"&gt;Notification System Architecture Deep Dive — Idempotency &amp;amp; Duplicate Suppression&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Provider Failover: A Timeout Is Not a Rejection
&lt;/h2&gt;

&lt;p&gt;Suppose the primary SMS provider times out. Should we immediately send through a backup provider?&lt;/p&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;The primary may have already accepted the request but lost the response. Sending through a second provider can produce two real messages.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;known rejection&lt;/strong&gt; is different from an &lt;strong&gt;ambiguous outcome&lt;/strong&gt;. Failover policy should consider the signal, message urgency, duplicate tolerance, and the backup provider's capacity.&lt;/p&gt;

&lt;p&gt;For a security alert, accepting some duplicate risk might be reasonable to improve the chance of timely delivery. For a promotional notification, conservative retry or reconciliation may be preferable.&lt;/p&gt;

&lt;p&gt;Provider failover is a trade-off, not a universal on/off switch. The &lt;a href="https://seeitflow.com/system-design/foundations/notification/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions&lt;/a&gt; page compares failover versus retrying the primary in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Delivery Tracking: SENT Is Not DELIVERED
&lt;/h2&gt;

&lt;p&gt;The system should maintain an observable notification lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACCEPTED → QUEUED → PROCESSING → PROVIDER_ACCEPTED
                                     |
                                     v
                                  DELIVERED

Alternative outcomes:
  RETRY_SCHEDULED | FAILED_PERMANENT | EXPIRED
  SUPPRESSED | BOUNCED | UNDELIVERABLE | DLQ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PROVIDER_ACCEPTED&lt;/code&gt; means the provider took the request. It does &lt;strong&gt;not&lt;/strong&gt; prove that the recipient received it.&lt;/p&gt;

&lt;p&gt;Providers may send delivery-status webhooks later, but those callbacks can arrive duplicated, delayed, out of order, or not at all.&lt;/p&gt;

&lt;p&gt;A robust reconciliation pipeline should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authenticate provider callbacks.&lt;/li&gt;
&lt;li&gt;Deduplicate callback events.&lt;/li&gt;
&lt;li&gt;Apply idempotent, monotonic state transitions.&lt;/li&gt;
&lt;li&gt;Prevent a late &lt;code&gt;SENT&lt;/code&gt; callback from overwriting &lt;code&gt;DELIVERED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Query provider status APIs when callbacks are missing and such APIs are available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every channel provides a reliable end-device delivery receipt. The status model must describe what the system actually knows, rather than claim more than a provider can prove.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Production Failure Scenarios: Where the Architecture Gets Tested
&lt;/h2&gt;

&lt;p&gt;A notification system's quality becomes clear when something breaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Email provider outage
&lt;/h3&gt;

&lt;p&gt;Email workers back off and accumulate a durable backlog. Push and SMS continue independently. Recovery is paced according to provider capacity and message deadlines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broker outage
&lt;/h3&gt;

&lt;p&gt;With a transactional outbox, the API can continue durably recording notification intent while the broker is down, subject to database capacity and the API's acceptance contract. Publishing resumes when the broker recovers. Without a durable fallback, the API should not falsely report successful acceptance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Massive promotional campaign
&lt;/h3&gt;

&lt;p&gt;A campaign targeting millions of recipients should be expanded into &lt;strong&gt;bounded fanout chunks&lt;/strong&gt;, paced to sustainable capacity. Transactional and security traffic must retain protected capacity. The campaign should support cancellation and expiry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Priority starvation
&lt;/h3&gt;

&lt;p&gt;A strict high-first scheduler can leave normal and low-priority notifications waiting forever. Monitor &lt;strong&gt;oldest-message age by priority&lt;/strong&gt;, not just queue depth, and use fair scheduling or reserved capacity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delivery tracker outage
&lt;/h3&gt;

&lt;p&gt;A well-isolated tracker failure should degrade status visibility rather than stop all delivery. But provider IDs and state events must be preserved for later reconciliation; simply losing them creates a permanent audit gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backlog recovery storm
&lt;/h3&gt;

&lt;p&gt;After a long outage, releasing every queued notification at once can overwhelm a recovering provider. Drain gradually, respect priority, and discard or expire messages that are no longer useful.&lt;/p&gt;

&lt;p&gt;These are part of the &lt;strong&gt;14 concrete incidents&lt;/strong&gt; covered in &lt;a href="https://seeitflow.com/system-design/foundations/notification/failure-scale-scenarios" rel="noopener noreferrer"&gt;Notification System Failure &amp;amp; Scale Scenarios&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. How Would We Evolve This Architecture?
&lt;/h2&gt;

&lt;p&gt;A good system design grows in response to evidence, not because a diagram looks more impressive with more components.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Problem it solves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Simple notification service&lt;/td&gt;
&lt;td&gt;Basic low-volume delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Durable ingestion and asynchronous workers&lt;/td&gt;
&lt;td&gt;Provider latency and failure isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Per-channel queues, preferences, priority scheduling&lt;/td&gt;
&lt;td&gt;Multi-channel correctness and urgent-message latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Shared provider rate limits, retry scheduling, DLQ&lt;/td&gt;
&lt;td&gt;Throttling, transient failures, and observability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Idempotency, reconciliation, campaign pacing&lt;/td&gt;
&lt;td&gt;Duplicate reduction, trustworthy state, and large-scale fanout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The precise order depends on product requirements. For example, recipient opt-outs and basic idempotency may be mandatory from the beginning rather than later enhancements.&lt;/p&gt;

&lt;h2&gt;
  
  
  17. Notification System Design Interview Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why use a queue in a notification system?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
To decouple the rate of notification creation from the rate of provider delivery, keep producer latency independent of provider health, and buffer temporary bursts durably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Kafka or SQS guarantee exactly-once notification delivery?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
No. Internal broker semantics cannot eliminate the ambiguous external provider-call window. Use at-least-once processing, idempotent internal transitions, provider idempotency where available, and reconciliation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do security alerts bypass promotional notifications?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use priority classes with protected capacity or fair scheduling, so urgent traffic gets preferential latency without starving other classes indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the provider allows only 1,000 messages per second?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Coordinate a rate-limit budget across workers. Additional workers cannot bypass the provider's account-level ceiling. Pace incoming campaigns and monitor backlog age.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do retries need jitter?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Exponential backoff alone can leave workers synchronized. Jitter spreads attempts across time and reduces retry spikes during recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should we fail over to a second provider after a timeout?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Only after considering whether the outcome is ambiguous, how urgent the notification is, and whether duplicate delivery is acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we send 100 million promotional notifications without delaying OTPs?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Accept the campaign quickly, enumerate recipients asynchronously in bounded chunks, pace fanout, and reserve downstream capacity for time-sensitive traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between SENT and DELIVERED?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
SENT or PROVIDER_ACCEPTED means the external provider accepted the request; DELIVERED requires a stronger provider or downstream confirmation, where supported.&lt;/p&gt;

&lt;p&gt;For more advanced follow-ups on outbox consistency, webhook ordering, push-token invalidation, and dedup-store failures, see the &lt;a href="https://seeitflow.com/system-design/foundations/notification/interview-perspective" rel="noopener noreferrer"&gt;Notification System Interview Perspective&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The hardest part of a notification system isn't choosing Kafka over RabbitMQ or integrating an SMS SDK.&lt;/p&gt;

&lt;p&gt;It's recognizing that &lt;strong&gt;notification creation, delivery, provider acceptance, and confirmed receipt are different events with different guarantees&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A durable queue protects producers from provider slowness. Channel isolation contains failures. Fair scheduling protects critical traffic. Rate limiting respects real provider ceilings. Backoff, jitter, deadlines, and DLQs make recovery controlled rather than chaotic. Idempotency and reconciliation reduce duplicate risk without pretending external side effects can be made universally exactly-once.&lt;/p&gt;

&lt;p&gt;The architecture becomes easier to reason about when each component exists to solve a specific failure mode.&lt;/p&gt;

&lt;p&gt;That's the deeper lesson of notification system design: &lt;strong&gt;reliability comes from making failure boundaries explicit, not from adding more infrastructure.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Reading
&lt;/h2&gt;

&lt;p&gt;These SeeItFlow resources explore the same system in more detail:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/notification/complete-design" rel="noopener noreferrer"&gt;Notification System — Complete Design&lt;/a&gt;&lt;/strong&gt; — Requirements, API contract, queue-based evolution, channel routing, priorities, retries, DLQ, and final architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/notification/architecture-deep-dive" rel="noopener noreferrer"&gt;Notification System — Architecture Deep Dive&lt;/a&gt;&lt;/strong&gt; — Transactional outbox, three idempotency boundaries, fair scheduling, recipient policy, push tokens, templates, distributed rate limiting, provider adapters, webhooks, campaign fanout, and delivery-state storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/notification/tradeoffs-decisions" rel="noopener noreferrer"&gt;Notification System — Trade-offs &amp;amp; Decisions&lt;/a&gt;&lt;/strong&gt; — Sync versus async, queue topology, retry policies, rate-limit coordination, provider failover, and template-rendering decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/notification/failure-scale-scenarios" rel="noopener noreferrer"&gt;Notification System — Failure &amp;amp; Scale Scenarios&lt;/a&gt;&lt;/strong&gt; — Fourteen operational scenarios covering provider outages, retry storms, queue failures, throttling, duplicate events, starvation, and backlog recovery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/notification/interview-perspective" rel="noopener noreferrer"&gt;Notification System — Interview Perspective&lt;/a&gt;&lt;/strong&gt; — Interview answer structure, common mistakes, and advanced follow-up questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/notification/visual-design-journey" rel="noopener noreferrer"&gt;Notification System — Visual Journey&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Explore more free, visual explanations of distributed systems, backend engineering, and system design at &lt;strong&gt;&lt;a href="https://seeitflow.com/" rel="noopener noreferrer"&gt;SeeItFlow&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>backend</category>
      <category>architecture</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Designing a URL Shortener: Architecture, Base62, Redis Caching &amp; Scaling</title>
      <dc:creator>MANGESH MANDLIK</dc:creator>
      <pubDate>Thu, 08 Oct 2026 07:51:19 +0000</pubDate>
      <link>https://dev.to/mangeshmandlik/designing-a-url-shortener-architecture-base62-redis-caching-scaling-576n</link>
      <guid>https://dev.to/mangeshmandlik/designing-a-url-shortener-architecture-base62-redis-caching-scaling-576n</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9al1yl32ow14ihfqsol.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9al1yl32ow14ihfqsol.png" alt="final architecture" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A URL shortener sounds like one of the simplest backend systems to build.&lt;/p&gt;

&lt;p&gt;Take a long URL:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://example.com/articles/distributed-systems/introduction&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Generate a short code:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://short.ly/aB7xK2p&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Store the mapping, and redirect anyone who visits the short URL.&lt;/p&gt;

&lt;p&gt;Three operations. One database table. A few API endpoints.&lt;/p&gt;

&lt;p&gt;So why is URL Shortener System Design such a useful distributed systems interview problem?&lt;/p&gt;

&lt;p&gt;Because generating the short URL is rarely the difficult part.&lt;/p&gt;

&lt;p&gt;The interesting engineering begins when a link gets shared with millions of people, thousands of requests hit the same database record, a cache restarts during peak traffic, or the analytics pipeline starts slowing down redirects.&lt;/p&gt;

&lt;p&gt;A production-grade URL shortener is fundamentally a problem of &lt;strong&gt;read-heavy architecture, uniqueness, caching, consistency, and failure isolation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's design one from first principles.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What Are We Actually Building?
&lt;/h2&gt;

&lt;p&gt;Before choosing Redis, Kafka, or a particular database, we need to understand what the system must do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Functional requirements
&lt;/h3&gt;

&lt;p&gt;Our URL shortening service should support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a unique short URL from a long URL.&lt;/li&gt;
&lt;li&gt;Redirecting visitors to the original destination.&lt;/li&gt;
&lt;li&gt;Optional custom aliases, such as &lt;code&gt;short.ly/summer-sale&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Expiring links after a specified duration.&lt;/li&gt;
&lt;li&gt;Tracking click analytics, including counts and trends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will keep authentication, billing, QR codes, and advanced link-management features outside the initial scope.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-functional requirements
&lt;/h3&gt;

&lt;p&gt;The system should provide low-latency redirects, high availability, durable URL mappings, and the ability to absorb sudden traffic spikes.&lt;/p&gt;

&lt;p&gt;One requirement is especially important: &lt;strong&gt;redirect availability matters more than URL creation availability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If users cannot create new links for a few minutes, that is inconvenient. If existing links stop redirecting, every application, email, or document containing those links can be affected.&lt;/p&gt;

&lt;p&gt;That difference will influence how we design the entire system.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Capacity Estimation: Why Reads Dominate the Architecture
&lt;/h2&gt;

&lt;p&gt;Let's establish an illustrative workload.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Design assumption&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stored URL mappings&lt;/td&gt;
&lt;td&gt;100 million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily redirects&lt;/td&gt;
&lt;td&gt;100 million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read-to-write ratio&lt;/td&gt;
&lt;td&gt;Approximately 100:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average mapping storage&lt;/td&gt;
&lt;td&gt;500 bytes including overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak traffic&lt;/td&gt;
&lt;td&gt;Up to 100× average redirect rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirect latency&lt;/td&gt;
&lt;td&gt;Low milliseconds on the cache-hit path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are assumptions for our design exercise, not universal URL-shortener benchmarks.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much storage do we need?
&lt;/h3&gt;

&lt;p&gt;With 100 million mappings at approximately 500 bytes each:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;100,000,000 × 500 bytes ≈ 50 GB&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That is a manageable amount of mapping data for a modern database deployment, although indexes, replicas, backups, and future growth require additional capacity.&lt;/p&gt;

&lt;p&gt;Storage is not necessarily our first scaling problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many redirects must we serve?
&lt;/h3&gt;

&lt;p&gt;100 million redirects per day translates to approximately:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;100,000,000 / 86,400 ≈ 1,157 redirects/second&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now imagine a major traffic event pushes peak demand toward 100,000 redirects per second.&lt;/p&gt;

&lt;p&gt;If every redirect queries the database, the database repeatedly performs lookups for the same popular links.&lt;/p&gt;

&lt;p&gt;The system isn't doing complicated work. It is doing simple, repetitive work far too often.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first architectural optimization should target repeated reads, not immediately introduce database sharding.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a more complete capacity model, see &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/complete-design" rel="noopener noreferrer"&gt;URL Shortener — Complete System Design&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Start With the Simplest Possible Architecture
&lt;/h2&gt;

&lt;p&gt;Our first version needs only three components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   |
   v
URL Shortener API
   |
   v
Relational Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application exposes two core operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /shorten
Content-Type: application/json

{
  "long_url": "https://example.com/article"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://short.ly/aB7xK2p"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for redirects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /aB7xK2p
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service looks up the mapping and returns an appropriate HTTP redirect response with a &lt;code&gt;Location&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Our initial database schema can be remarkably small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;url_mappings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;short_code&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;long_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary-key index supports fast lookups by short code and enforces uniqueness.&lt;/p&gt;

&lt;p&gt;This architecture is perfectly reasonable for an early-stage product.&lt;/p&gt;

&lt;p&gt;There is no reason to operate Redis clusters, distributed ID generators, or multiple database regions before the workload requires them.&lt;/p&gt;

&lt;p&gt;But once popular URLs receive large numbers of repeated requests, our database begins doing unnecessary work.&lt;/p&gt;

&lt;p&gt;That is the first bottleneck we should solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. How Do URL Shorteners Generate Unique Short Codes?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wirhufoj3a1iun8ajoc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wirhufoj3a1iun8ajoc.png" alt="approaches to generate short code" width="799" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The short code is the identity of a URL mapping.&lt;/p&gt;

&lt;p&gt;We need it to be compact, unique, and ideally difficult to guess if enumeration is a concern.&lt;/p&gt;

&lt;p&gt;There are several approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach A: Auto-increment IDs with Base62
&lt;/h3&gt;

&lt;p&gt;Suppose the database generates an integer ID.&lt;/p&gt;

&lt;p&gt;We convert that integer into Base62, using:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0-9&lt;/code&gt;, &lt;code&gt;a-z&lt;/code&gt;, and &lt;code&gt;A-Z&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This produces compact, URL-safe representations.&lt;/p&gt;

&lt;p&gt;Advantages include straightforward uniqueness and minimal collision handling.&lt;/p&gt;

&lt;p&gt;The disadvantage is predictability. Sequential identifiers can be enumerated, even when encoded in Base62.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach B: Hash the original URL
&lt;/h3&gt;

&lt;p&gt;We could hash the long URL and use part of the resulting value as the short code.&lt;/p&gt;

&lt;p&gt;This can support deterministic mappings, where the same input produces the same output.&lt;/p&gt;

&lt;p&gt;But truncated hashes can collide.&lt;/p&gt;

&lt;p&gt;It also creates a product question: should two different users shortening the same destination receive the same code?&lt;/p&gt;

&lt;p&gt;For campaign tracking, ownership, or independent expiry policies, they may need different codes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach C: Random Base62
&lt;/h3&gt;

&lt;p&gt;Generate a random code from the Base62 alphabet.&lt;/p&gt;

&lt;p&gt;The available namespace grows exponentially with code length.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code length&lt;/th&gt;
&lt;th&gt;Possible codes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6 characters&lt;/td&gt;
&lt;td&gt;56.8 billion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 characters&lt;/td&gt;
&lt;td&gt;3.52 trillion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 characters&lt;/td&gt;
&lt;td&gt;218 trillion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A seven-character code offers a large namespace for our illustrative workload.&lt;/p&gt;

&lt;p&gt;But large does not mean collision-free.&lt;/p&gt;

&lt;p&gt;With 100 million occupied codes in a seven-character namespace, the probability that the next uniformly random attempt collides is approximately:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;100,000,000 / 62^7 ≈ 0.00284%&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That is a small per-attempt probability, but collisions will still occur over a sufficiently large number of creations.&lt;/p&gt;

&lt;p&gt;The solution is not to pretend collisions are impossible.&lt;/p&gt;

&lt;p&gt;It is to make them harmless.&lt;/p&gt;

&lt;p&gt;Use a database uniqueness constraint, and retry with a new random code when a collision occurs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate random Base62 code
           |
           v
Attempt database INSERT
           |
      +----+----+
      |         |
   Success   Collision
      |         |
    Return   Generate again
              (bounded retries)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry count must be bounded. An unexpected increase in collisions should be observable rather than producing an infinite loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach D: Snowflake-style distributed IDs
&lt;/h3&gt;

&lt;p&gt;A Snowflake-style generator combines information such as a timestamp, worker identifier, and sequence counter to generate unique IDs without coordinating with the database on every allocation.&lt;/p&gt;

&lt;p&gt;It can be useful for very high creation throughput or independent generation across many nodes.&lt;/p&gt;

&lt;p&gt;However, it introduces clock-handling and worker-ID management requirements.&lt;/p&gt;

&lt;p&gt;And importantly, Base62-encoding a Snowflake ID does not make it unpredictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our choice:&lt;/strong&gt; For this workload, random Base62 with a database uniqueness constraint and bounded retries is a sensible starting point.&lt;/p&gt;

&lt;p&gt;Snowflake is an alternative when decentralized ID allocation becomes a genuine requirement, not an automatic upgrade merely because multiple application servers exist.&lt;/p&gt;

&lt;p&gt;For collision mathematics, namespace sizing, and distributed ID mechanics, see &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/architecture-deep-dive" rel="noopener noreferrer"&gt;URL Shortener — Architecture Deep Dive&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Choosing the Database: SQL or NoSQL?
&lt;/h2&gt;

&lt;p&gt;A common system design question is:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Should a URL shortener use PostgreSQL, MySQL, DynamoDB, or another distributed key-value store?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer depends on the workload.&lt;/p&gt;

&lt;p&gt;Our dominant query is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;long_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_mappings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;short_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'aB7xK2p'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need exact-key lookups, durable writes, and reliable uniqueness enforcement.&lt;/p&gt;

&lt;p&gt;A relational database such as PostgreSQL or MySQL handles these requirements well.&lt;/p&gt;

&lt;p&gt;A distributed key-value database can also be appropriate, especially when horizontal write scaling, global distribution, or multi-region availability becomes a stronger requirement.&lt;/p&gt;

&lt;p&gt;But choosing NoSQL solely because the application might eventually serve millions of redirects is not sufficient reasoning.&lt;/p&gt;

&lt;p&gt;If most redirects are served from cache, the database never sees most of that traffic.&lt;/p&gt;

&lt;p&gt;We should scale the database based on its actual workload, not the public request rate.&lt;/p&gt;

&lt;p&gt;There is another important trade-off: database replicas.&lt;/p&gt;

&lt;p&gt;Routing cache misses to read replicas protects the primary's write capacity. However, asynchronously replicated data may be temporarily stale.&lt;/p&gt;

&lt;p&gt;A short URL created moments ago might exist on the primary but not yet on a replica.&lt;/p&gt;

&lt;p&gt;If the cache misses during that window, the system could incorrectly return 404.&lt;/p&gt;

&lt;p&gt;Eager cache population after a successful write, read-after-write routing, or stronger replication guarantees can mitigate this, depending on the consistency requirement.&lt;/p&gt;

&lt;p&gt;The detailed alternatives are compared in &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/tradeoffs-decisions" rel="noopener noreferrer"&gt;URL Shortener — Trade-offs &amp;amp; Decisions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Redis Caching: The Most Important Scaling Decision
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nnmgejqdztcrxemzl25.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nnmgejqdztcrxemzl25.png" alt="with caching" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we address our main bottleneck.&lt;/p&gt;

&lt;p&gt;Without caching, every redirect requires a database lookup.&lt;/p&gt;

&lt;p&gt;With Redis, popular mappings can be served from memory.&lt;/p&gt;

&lt;p&gt;The request path becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   |
   v
Load Balancer
   |
   v
URL Service
   |
   +----&amp;gt; Redis
   |        |
   |     Cache Hit ----&amp;gt; Redirect
   |
   +----&amp;gt; Cache Miss
              |
              v
         Database
              |
              v
        Populate Redis
              |
              v
           Redirect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cache-aside pattern.&lt;/p&gt;

&lt;p&gt;On URL creation, we can also populate Redis immediately after the database successfully commits the mapping.&lt;/p&gt;

&lt;p&gt;That reduces the chance that a newly created link experiences a cold-cache miss when its first visitors arrive.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much database traffic can caching eliminate?
&lt;/h3&gt;

&lt;p&gt;Assume 100,000 redirects per second and a measured or estimated cache-hit ratio of 92%.&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Database reads = 100,000 × (1 - 0.92)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Database reads ≈ 8,000 per second&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The cache absorbs approximately 92,000 reads per second.&lt;/p&gt;

&lt;p&gt;This illustrates why caching can dramatically reduce database pressure. The actual hit ratio must be measured; it depends on traffic distribution, memory capacity, and expiry policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  The subtle problem: cache expiry correctness
&lt;/h3&gt;

&lt;p&gt;Suppose a short URL expires in 10 minutes.&lt;/p&gt;

&lt;p&gt;But Redis stores the mapping with a fixed TTL of 24 hours.&lt;/p&gt;

&lt;p&gt;After 10 minutes, the authoritative mapping is expired. Yet Redis may continue redirecting users for almost another day.&lt;/p&gt;

&lt;p&gt;This is a correctness bug, not just a caching inefficiency.&lt;/p&gt;

&lt;p&gt;A safer TTL rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cache_ttl = min(
    configured_cache_ttl,
    expires_at - current_time
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For non-expiring mappings, use the configured cache TTL.&lt;/p&gt;

&lt;p&gt;The redirect handler should also check logical expiry when that metadata is available.&lt;/p&gt;

&lt;p&gt;Explicit updates or deletions require cache invalidation rather than relying exclusively on TTL expiry.&lt;/p&gt;

&lt;p&gt;The complete cache hierarchy, expiry rules, and invalidation mechanisms are covered in the &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What Happens When a Short URL Goes Viral?
&lt;/h2&gt;

&lt;p&gt;Imagine a short link appears in a major social-media post.&lt;/p&gt;

&lt;p&gt;Within seconds, hundreds of thousands of people click it.&lt;/p&gt;

&lt;p&gt;A warm Redis cache helps because most requests avoid the database.&lt;/p&gt;

&lt;p&gt;But a different problem can appear.&lt;/p&gt;

&lt;p&gt;Every request for the same short code may hit the same Redis key.&lt;/p&gt;

&lt;p&gt;Even in a sharded Redis cluster, that key is generally owned by one shard.&lt;/p&gt;

&lt;p&gt;Adding more shards distributes different keys. It does not automatically distribute reads for one extremely popular key.&lt;/p&gt;

&lt;p&gt;This is called a &lt;strong&gt;hot-key problem&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do we solve it?
&lt;/h3&gt;

&lt;p&gt;One option is a small process-local cache inside each URL service instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  Load Balancer
                       |
          +------------+------------+
          |            |            |
       Service A    Service B    Service C
          |            |            |
       Local Cache  Local Cache  Local Cache
          |            |            |
          +------------+------------+
                       |
                     Redis
                       |
                    Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If each application instance caches the extremely popular mapping locally, many requests no longer reach Redis.&lt;/p&gt;

&lt;p&gt;Other options include serving reads through cache replicas or using CDN edge caching for genuinely immutable redirects.&lt;/p&gt;

&lt;p&gt;Each choice adds different consistency and invalidation considerations.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if Redis restarts during the viral spike?
&lt;/h3&gt;

&lt;p&gt;Now the cache is empty.&lt;/p&gt;

&lt;p&gt;Thousands of requests for the same URL can simultaneously miss and query the database.&lt;/p&gt;

&lt;p&gt;That is a &lt;strong&gt;cache stampede&lt;/strong&gt;, also called a thundering herd.&lt;/p&gt;

&lt;p&gt;Request coalescing solves an important part of the problem: concurrent misses for the same code share one in-flight database lookup rather than issuing duplicate queries.&lt;/p&gt;

&lt;p&gt;TTL jitter and proactive warming of known hot keys can reduce the risk further.&lt;/p&gt;

&lt;p&gt;A viral URL, a Redis hot key, and a cold-cache stampede are related but distinct operational scenarios.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/failure-scale-scenarios" rel="noopener noreferrer"&gt;URL Shortener — Failure &amp;amp; Scale Scenarios&lt;/a&gt; for the incident-by-incident breakdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. 301 vs 302 Redirect: A Small Decision With Big Consequences
&lt;/h2&gt;

&lt;p&gt;Should a URL shortener return &lt;code&gt;301 Moved Permanently&lt;/code&gt; or &lt;code&gt;302 Found&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;There is no universally correct answer.&lt;/p&gt;

&lt;p&gt;A permanent redirect communicates that the destination is intended to remain stable.&lt;/p&gt;

&lt;p&gt;A temporary redirect communicates that the destination may change.&lt;/p&gt;

&lt;p&gt;The choice affects browser caching, infrastructure load, and analytics visibility.&lt;/p&gt;

&lt;p&gt;For example, if a browser caches a permanent redirect, later visits may bypass the URL shortener entirely.&lt;/p&gt;

&lt;p&gt;That reduces load on our infrastructure.&lt;/p&gt;

&lt;p&gt;But those visits may also become invisible to origin-side click analytics.&lt;/p&gt;

&lt;p&gt;If a short URL's destination can be edited, long-lived client caching becomes more problematic because visitors may continue reaching an old destination.&lt;/p&gt;

&lt;p&gt;Temporary redirects can keep requests visible to the service, provided the caching headers and client behavior are configured accordingly.&lt;/p&gt;

&lt;p&gt;Remember that HTTP caching depends on &lt;strong&gt;status code, response headers, and client behavior together&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is incorrect to assume that every 301 is cached forever or that a 302 can never be cached.&lt;/p&gt;

&lt;p&gt;For mutable destinations or products prioritizing click visibility, a temporary redirect posture is often easier to manage.&lt;/p&gt;

&lt;p&gt;For immutable, heavily accessed links, permanent redirects with appropriate caching policies may reduce origin traffic significantly.&lt;/p&gt;

&lt;p&gt;This decision is explored further in &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/tradeoffs-decisions" rel="noopener noreferrer"&gt;Trade-offs &amp;amp; Decisions — Permanent vs Temporary Redirect&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Click Analytics Should Not Slow Down Redirects
&lt;/h2&gt;

&lt;p&gt;A URL shortener may need to record every click for dashboards, reporting, or campaign analytics.&lt;/p&gt;

&lt;p&gt;The naive approach is to insert an analytics record before returning each redirect.&lt;/p&gt;

&lt;p&gt;But that makes redirect latency depend on the analytics database.&lt;/p&gt;

&lt;p&gt;At 100,000 redirects per second, we could generate 100,000 analytics events per second.&lt;/p&gt;

&lt;p&gt;Worse, if the analytics store slows down, the redirect path slows down too.&lt;/p&gt;

&lt;p&gt;These are two workloads with different requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redirect serving needs low latency and high availability.&lt;/li&gt;
&lt;li&gt;Analytics processing needs high throughput and can often tolerate delayed aggregation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We should isolate them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   URL Service
                   /         \
                  /           \
           Return Redirect   Async Event
                                  |
                                  v
                            Kafka / SQS
                                  |
                                  v
                          Analytics Workers
                                  |
                                  v
                           Analytics Store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The redirect service emits an event through a bounded, asynchronous mechanism.&lt;/p&gt;

&lt;p&gt;Workers consume events, batch them, and write them into an analytics store such as ClickHouse.&lt;/p&gt;

&lt;p&gt;But asynchronous processing is not automatically safe.&lt;/p&gt;

&lt;p&gt;What happens if Kafka or SQS becomes unavailable?&lt;/p&gt;

&lt;p&gt;The service must have an explicit policy: drop, sample, or temporarily buffer events within strict limits, depending on whether analytics loss is acceptable.&lt;/p&gt;

&lt;p&gt;The redirect should not block indefinitely waiting for analytics.&lt;/p&gt;

&lt;p&gt;If the product requires exact, lossless click counts, the design needs stronger event durability and deduplication guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Another subtle scaling issue: analytics partitioning
&lt;/h3&gt;

&lt;p&gt;If we partition events by short code, all clicks for one viral URL may land on the same partition.&lt;/p&gt;

&lt;p&gt;That creates a hot partition even if the rest of the event-streaming cluster has available capacity.&lt;/p&gt;

&lt;p&gt;If the analytics workload primarily computes counts and aggregates, distributing events more broadly and merging counts downstream may be preferable.&lt;/p&gt;

&lt;p&gt;Strict per-URL event ordering is not always necessary.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/architecture-deep-dive" rel="noopener noreferrer"&gt;Architecture Deep Dive&lt;/a&gt; covers event-delivery guarantees, partitioning alternatives, and duplicate-count handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. The Production Architecture
&lt;/h2&gt;

&lt;p&gt;We can now assemble the architecture based on the bottlenecks we have actually identified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     Clients
                        |
                        v
                   Load Balancer
                        |
                        v
              Stateless URL Services
              /          |         \
             /           |          \
       Local Cache     Redis     Analytics Events
             |           |              |
             +-----+-----+              v
                   |                 Kafka / SQS
                   v                     |
             Database Layer             v
              /         \         Analytics Workers
          Primary     Replicas           |
                                        v
                                  Analytics Store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component has a specific responsibility.&lt;/p&gt;

&lt;p&gt;The load balancer distributes requests across stateless application instances.&lt;/p&gt;

&lt;p&gt;The URL services handle validation, short-code generation, cache lookups, expiry checks, and redirects.&lt;/p&gt;

&lt;p&gt;Redis absorbs repeated mapping reads, while optional local caches protect extremely hot keys.&lt;/p&gt;

&lt;p&gt;The relational database remains the authoritative source of URL mappings and uniqueness.&lt;/p&gt;

&lt;p&gt;The event pipeline processes analytics independently.&lt;/p&gt;

&lt;p&gt;This is a logical architecture, not a prescription for a fixed number of servers, replicas, or cache nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal is not to add as many distributed components as possible. It is to introduce each component when it solves a demonstrated problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To understand how the simple database-only design evolves into this architecture, explore the &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/visual-design-journey" rel="noopener noreferrer"&gt;interactive URL Shortener Visual Design Journey&lt;/a&gt; on SeeItFlow.&lt;/p&gt;

&lt;p&gt;It walks through the architecture's evolution with animated, narrated explanations rather than relying only on a static diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Production Failures That Reveal Whether the Design Is Correct
&lt;/h2&gt;

&lt;p&gt;A system design is incomplete until we understand what happens when dependencies fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario A: Redis becomes unavailable
&lt;/h3&gt;

&lt;p&gt;All requests that previously hit Redis may fall through to the database.&lt;/p&gt;

&lt;p&gt;Redirects can continue if the database has sufficient capacity.&lt;/p&gt;

&lt;p&gt;But if it cannot absorb the sudden read increase, a cache outage can cascade into a database outage.&lt;/p&gt;

&lt;p&gt;Circuit breakers, bounded database concurrency, request coalescing, and local caching help contain the damage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario B: The database primary fails
&lt;/h3&gt;

&lt;p&gt;New URL creation becomes unavailable until a writable primary is restored or a replica is promoted.&lt;/p&gt;

&lt;p&gt;Existing redirects may continue through Redis and healthy read replicas.&lt;/p&gt;

&lt;p&gt;The exact impact depends on the replication topology and the freshness of available cached data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario C: A newly created URL returns 404
&lt;/h3&gt;

&lt;p&gt;The write succeeded on the primary, but a cache miss reached a lagging read replica.&lt;/p&gt;

&lt;p&gt;The replica has not yet received the new mapping.&lt;/p&gt;

&lt;p&gt;This is a read-after-write consistency problem.&lt;/p&gt;

&lt;p&gt;Eager cache population, primary reads for recently created mappings, or stronger replication guarantees can mitigate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario D: Two users request the same custom alias
&lt;/h3&gt;

&lt;p&gt;Both requests check availability and initially find the alias unused.&lt;/p&gt;

&lt;p&gt;Both attempt to create it.&lt;/p&gt;

&lt;p&gt;An application-level availability check cannot safely resolve this race.&lt;/p&gt;

&lt;p&gt;The database uniqueness constraint must arbitrate the conflict atomically. One request succeeds, while the other receives a conflict response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario E: The analytics broker fails
&lt;/h3&gt;

&lt;p&gt;If event publishing blocks indefinitely, analytics infrastructure can take down redirect serving.&lt;/p&gt;

&lt;p&gt;A bounded, explicitly defined producer failure policy keeps the two availability domains separated.&lt;/p&gt;

&lt;p&gt;These are only some of the important cases.&lt;/p&gt;

&lt;p&gt;The full &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/failure-scale-scenarios" rel="noopener noreferrer"&gt;Failure &amp;amp; Scale Scenarios&lt;/a&gt; reference covers Redis failures, cache stampedes, viral spikes, replica lag, namespace pressure, expired mappings, broker outages, and abuse traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. How Would This Architecture Evolve Over Time?
&lt;/h2&gt;

&lt;p&gt;We should not deploy the final architecture on day one.&lt;/p&gt;

&lt;p&gt;A more sensible progression is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Reason to evolve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Application + relational database&lt;/td&gt;
&lt;td&gt;Initial product with manageable traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Add Redis cache&lt;/td&gt;
&lt;td&gt;Repeated redirect reads begin stressing the database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Stateless services, replicas, async analytics&lt;/td&gt;
&lt;td&gt;Application throughput and analytics isolation become important&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Local hot-key caching and additional capacity&lt;/td&gt;
&lt;td&gt;Concentrated viral traffic exceeds shared-cache or application capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Multi-region reads or writes&lt;/td&gt;
&lt;td&gt;Geographic latency, availability, or independent regional write requirements justify the complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trigger for each transition should be a measured bottleneck or a new product requirement.&lt;/p&gt;

&lt;p&gt;Multi-region writes, for example, introduce global uniqueness, replication consistency, and cross-region invalidation problems.&lt;/p&gt;

&lt;p&gt;A single write region with geographically distributed reads may be sufficient for many products.&lt;/p&gt;

&lt;p&gt;Adding active-active writes merely because the system is large would create complexity without necessarily improving the user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. URL Shortener System Design Interview Questions
&lt;/h2&gt;

&lt;p&gt;Here are some of the most important questions to prepare for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is Redis useful in a URL shortener?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because a read-heavy workload with skewed access patterns repeatedly requests the same mappings. Redis can serve popular mappings from memory, reducing database load and redirect latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you prevent duplicate short URLs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For randomly generated codes, enforce uniqueness at the database layer and retry generation on a conflict. If the product requires deterministic deduplication of destination URLs, that is a separate rule with its own identity and concurrency requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Snowflake required for distributed ID generation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Random Base62 with database uniqueness enforcement or a database sequence can work across multiple application instances. Snowflake becomes useful when decentralized generation offers a concrete advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if Redis fails?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reads can fall back to the database, but the additional load must be bounded. Otherwise, the cache failure can trigger a cascading database overload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should the URL mapping database be SQL or NoSQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A relational database is a strong starting choice for exact-key lookups and uniqueness constraints. A distributed key-value store becomes more attractive when global distribution or horizontal write scalability justifies it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle millions of clicks on one short URL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keep the mapping cached, identify hot-key concentration, consider process-local caching or read replicas, and ensure the application tier and analytics producer can handle the request volume. The first bottleneck depends on the actual deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you ensure expired links stop redirecting?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enforce logical expiry on reads and ensure cache entries cannot outlive the authoritative mapping's expiry. Background row deletion alone is insufficient.&lt;/p&gt;

&lt;p&gt;For a much larger collection of follow-up questions and reasoning strategies, see &lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/interview-perspective" rel="noopener noreferrer"&gt;URL Shortener — Interview Perspective&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;A URL shortener teaches an important lesson about distributed systems.&lt;/p&gt;

&lt;p&gt;The difficult part is not generating a seven-character string or writing a database query.&lt;/p&gt;

&lt;p&gt;It is understanding how a small operation behaves when repeated millions of times under unpredictable conditions.&lt;/p&gt;

&lt;p&gt;The strongest architecture emerges from a sequence of questions:&lt;/p&gt;

&lt;p&gt;Why is the database doing the same lookup repeatedly?&lt;/p&gt;

&lt;p&gt;What happens if our cache disappears?&lt;/p&gt;

&lt;p&gt;Can a recently created URL temporarily return 404?&lt;/p&gt;

&lt;p&gt;Does our analytics pipeline have the power to break redirects?&lt;/p&gt;

&lt;p&gt;Are we solving a real scaling problem, or adding infrastructure because it looks sophisticated?&lt;/p&gt;

&lt;p&gt;Answering these questions leads to better architecture than memorizing a diagram.&lt;/p&gt;

&lt;p&gt;And that reasoning applies far beyond URL shorteners — to API gateways, caching systems, distributed databases, event-driven platforms, and other read-heavy services.&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Reading
&lt;/h2&gt;

&lt;p&gt;The following SeeItFlow resources explore the design in greater depth:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/complete-design" rel="noopener noreferrer"&gt;URL Shortener — Complete Design&lt;/a&gt;&lt;/strong&gt; — Requirements, capacity estimation, ID generation, caching, data model, final architecture, and production evolution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/visual-design-journey" rel="noopener noreferrer"&gt;URL Shortener — Visual Design Journey&lt;/a&gt;&lt;/strong&gt; — Interactive, narrated architectural evolution from a simple database-backed service to a distributed redirect system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/architecture-deep-dive" rel="noopener noreferrer"&gt;URL Shortener — Architecture Deep Dive&lt;/a&gt;&lt;/strong&gt; — Base62 namespace sizing, collision handling, Snowflake IDs, cache hierarchy, hot-key protection, database replication, and analytics partitioning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/tradeoffs-decisions" rel="noopener noreferrer"&gt;URL Shortener — Trade-offs &amp;amp; Decisions&lt;/a&gt;&lt;/strong&gt; — Alternative approaches and selection criteria for databases, redirects, caching, ID generation, and analytics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/failure-scale-scenarios" rel="noopener noreferrer"&gt;URL Shortener — Failure &amp;amp; Scale Scenarios&lt;/a&gt;&lt;/strong&gt; — Twelve production failure and scaling scenarios, including triggers, detection, mitigation, and recovery.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://seeitflow.com/system-design/foundations/url-shortener/interview-perspective" rel="noopener noreferrer"&gt;URL Shortener — Interview Perspective&lt;/a&gt;&lt;/strong&gt; — Interview preparation, architecture reasoning, common mistakes, and advanced follow-up questions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All six resources are part of &lt;a href="https://seeitflow.com/" rel="noopener noreferrer"&gt;SeeItFlow&lt;/a&gt;, a free visual software-engineering learning platform covering system design, distributed systems, backend engineering, databases, networking, and modern AI systems.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>distributed</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
