Course progress0%
Course content
65 min

Conflicts and Repeat Submissions

Use revisions to reject stale edits and request identities to deduplicate retries

By the end of this lesson

  • Use revisions to reject stale edits and request identities to deduplicate retries
  • State the limits of an in-memory confirmation simulation

Two different problems

A revision answers "Is this still the version I reviewed?" A request key answers "Have we already processed this same intention?" They are not interchangeable.

Build a deliberately small in-memory confirmation simulation for one server order. Start with { id: 'ORDER-1', status: 'draft', revision: 1 }. The server owns its items/customer; the confirmation request does not replace them. Restarting the server resets everything, so this lab cannot promise durable deduplication.

Contract before implementation

POST /api/orders/ORDER-1/confirm receives { "expectedRevision": 1, "requestId": "..." }. A successful response contains the confirmed order with revision 2. A repeated matching request returns the original success. Reusing the key with a different payload returns 409. A new request based on an obsolete revision also returns 409.

In this single-order lab, a factory scopes the keys to one order. A multi-user system would need explicit owner/operation scope, transactional persistence, retention rules and authorization.

structuredClone copies this plain data record, including its nested objects and arrays. Unlike the shallow spread copy studied in Course 02, it keeps returned records from sharing nested references with this store. It is suitable for these data-only fixtures, not a way to copy arbitrary functions or DOM elements. The processed Map remembers each successful request key and its result. Trace a first request and an identical retry through that map before connecting HTTP.

export function createConfirmationStore(initialOrder, validate) {
  let order = structuredClone(initialOrder);
  const processed = new Map();
  return {
    read: () => structuredClone(order),
    confirm(input) {
      if (!input || typeof input.requestId !== 'string' ||
          !input.requestId.trim() || input.requestId.length > 100 ||
          !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 1) {
        return { status: 422, body: { error: 'invalid_request' } };
      }
      const signature = JSON.stringify([order.id, input.expectedRevision]);
      const previous = processed.get(input.requestId);
      if (previous) {
        return previous.signature === signature
          ? structuredClone(previous.result)
          : { status: 409, body: { error: 'request_key_reused' } };
      }
      if (input.expectedRevision !== order.revision || order.status !== 'draft') {
        return { status: 409, body: { error: 'order_changed', order: structuredClone(order) } };
      }
      try { validate(order); }
      catch { return { status: 422, body: { error: 'invalid_order' } }; }
      order = { ...order, status: 'confirmed', revision: order.revision + 1 };
      const result = { status: 200, body: { order: structuredClone(order) } };
      processed.set(input.requestId, { signature, result });
      return structuredClone(result);
    }
  };
}

validate calls your domain validation/calculation for the stored order. This synchronous in-process operation has no await between checking and changing the order. Do not copy that assumption into a database implementation: the check, write and recorded request result must be atomic there.

Wire the lab

Instantiate the store once when the server starts, not per request. Add GET /api/orders/ORDER-1 using read(). Add the POST route with the bounded JSON reader from the previous lesson, and return the status/body from confirm. Expose neither store code nor its maps as static browser files.

The browser first reads this separate server fixture. Show its ID, status and revision so it cannot be confused with the local draft. Generate a request key once per new intention with crypto.randomUUID() in the local secure context. Keep the key, expected revision and snapshot for a retry after a lost response. On conflict, show the returned/current version and require a fresh review; do not silently resubmit against it.

Prove the distinction

Call confirm twice with the same key and revision 1: both responses must show the same confirmed revision 2. Use a new key with revision 1: expect conflict. Reuse the original key with revision 2: expect key-reuse conflict. Restart the server and observe that its memory is gone.

The final project's local editor and this server fixture are separate experiments. Connecting arbitrary draft creation to a durable order service is an extension, not an implied feature of this course.

Lesson complete?

Your progress is saved on this device.