C.W.K.
Stream
Lesson 02 of 04 · published

One Aggregate: Question + Images Together

~12 min · aggregate, atomicity, consistency, intent

Level 0Dead Zone
0 XP0/32 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"A question without its image is a different question. If they can be saved separately, they can be corrupted separately."

The Half-Saved Question

Imagine capturing the question and the image as two independent writes. The text saves; then the app is killed before the image blob is written. Now the outbox holds "Which of these is sharper?" with no image attached. When it finally syncs, Pippa gets a question that is literally unanswerable — the referent is gone. The user's intent has been silently corrupted, not by losing everything, but by losing part of one indivisible thing.

Capture-first fixes this by treating the question and all its attachments as a single aggregate: one unit that is written completely or not at all. There is no valid intermediate state where the text exists but an image doesn't. Either the whole turn is durable, or the submit hasn't happened yet.

Aggregate Means One Consistency Boundary

The word comes from domain modeling: an aggregate is a cluster of data that must stay internally consistent and is only ever modified as a whole. For a Pippa Go turn, the consistency rule is simple and strict — a question and its referenced images are meaningless apart. So they share one boundary:

  • One commit writes the question text and every image blob and the attachment references, together.
  • If that commit can't complete, it rolls back to nothing — no orphan text, no orphan blobs.
  • Downstream, delivery and retry always move the whole aggregate, never a fragment.

Why Atomicity Beats 'Just Re-attach'

A tempting shortcut is to let the pieces save separately and "reconcile later" — re-attach the image if it's missing. But reconciliation is where bugs breed: which image? attached to which question? what if two turns are in flight? The atomic aggregate deletes the entire problem class. There's no reconciliation because there was never a moment of inconsistency to reconcile. One write, one boundary, one intent — kept whole from the instant the user hit submit.

Code

One atomic write for the whole turn·typescript
// The turn and its image blobs commit in a single IndexedDB transaction.
async function commitLocally(turn: PippaGoTurn, blobs: Map<string, Blob>) {
  const tx = db.transaction(["turns", "blobs"], "readwrite");
  // All-or-nothing: if any put() fails, the whole tx aborts and rolls back.
  tx.objectStore("turns").put(turn);
  for (const [localId, blob] of blobs) {
    tx.objectStore("blobs").put({ localId, blob });
  }
  await tx.done; // resolves only if EVERYTHING committed together
}

// There is no state where turn.attachments references a blob
// that isn't in the store. The aggregate is consistent or absent.

External links

Exercise

List three real 'half-saved' failure modes for a question-with-image: text-without-image, image-without-text, image-attached-to-wrong-question. For each, describe what Pippa would receive and why the answer would be wrong. Then explain in one sentence how committing the whole turn atomically makes all three impossible rather than merely handled.
Hint
All three failure modes require a moment where part of the aggregate exists without the rest. An atomic commit has no such moment — the intermediate state simply doesn't exist, so none of the three can occur.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.