Coding problem · Medium · Design & Data Structures

Write-Behind View Counter with Safe Flushes

Medium Practical tasks

Video views arrive far too fast to write each one to the database, so they are buffered and flushed in batches (write-behind). Flushes can crash after claiming a batch, and a commit can be retried after a timeout. Implement the flushing protocol so no view is lost and none is counted twice.

ViewBuffer():

  • record(video, n) — add n views to the buffer.
  • flush() — if a claimed batch is still pending (its commit hasn't succeeded), return its id again without moving new data into it. Otherwise, if the buffer is empty return ""; else move the whole buffer into a new pending batch with id "b1", "b2", … and return the id.
  • commit(batchId) — the database transaction for a batch. If batchId is the pending batch, add its counts to the database totals, mark it committed, clear the pending batch and return true. Otherwise (already committed, or unknown) return false — committing twice must not double-count.
  • views(video) — the committed database total for the video (0 if none).

See Write-Through and Write-Behind.

Example 1

Input: ["ViewBuffer","record","record","flush","record","flush","commit","views","flush","commit","commit","views","views"]
[[],["v1",3],["v2",1],[],["v1",2],[],["b1"],["v1"],[],["b2"],["b2"],["v1"],["v2"]]
Output: [null,null,null,"b1",null,"b1",true,3,"b2",true,false,5,1]
Explanation: The second flush returns the still-pending b1; views recorded meanwhile go into b2. The retried commit of b2 is ignored.

Hints

Hint 1

Keep three things: the live buffer, at most one pending batch (id + counts), and the set of committed batch ids.

Hint 2

Never clear the pending batch until its commit succeeds.

Write and run your solution

Loading the editor… (JavaScript is required to run code in your browser.)

Solution and explanation

Show the solution

Time O(1) per record, O(videos in batch) per commit · Space O(videos + batches)

Two rules make write-behind safe: claim, don't remove — a batch stays pending (re-flushable) until its database commit succeeds, so a crash between claim and commit loses nothing; and idempotent commits — remembering committed batch ids (in the database, in the same transaction) turns a retried commit into a no-op. Redis Streams provide the first rule (pending entries until XACK); a processed_batches table with a unique key provides the second.

Reference solution

class ViewBuffer {
  constructor() {
    this.buffer = new Map();
    this.pending = null;          // { id, counts: Map }
    this.committed = new Set();
    this.db = new Map();
    this.nextId = 1;
  }

  record(video, n) {
    this.buffer.set(video, (this.buffer.get(video) ?? 0) + n);
  }

  flush() {
    if (this.pending) return this.pending.id;
    if (this.buffer.size === 0) return "";
    this.pending = { id: `b${this.nextId++}`, counts: this.buffer };
    this.buffer = new Map();
    return this.pending.id;
  }

  commit(batchId) {
    if (!this.pending || this.pending.id !== batchId || this.committed.has(batchId)) return false;
    for (const [video, n] of this.pending.counts) this.db.set(video, (this.db.get(video) ?? 0) + n);
    this.committed.add(batchId);
    this.pending = null;
    return true;
  }

  views(video) {
    return this.db.get(video) ?? 0;
  }
}

Common mistakes

  • Removing data from the buffer before the commit succeeds (lost views on crash).
  • Allowing a second commit of the same batch (double counting).
  • Moving new data into a batch that is already pending.

Follow-up questions

  • Persist committed batch ids in the database transaction.
  • Bound the buffer and apply backpressure when flushes fail.