Background work is usually introduced to make a system more reliable. We persist an intent, let the request finish, and deliver the external side effect later. That protects the user-facing path from temporary provider failures and gives us room for retries.
It also creates a time gap.
Inside that gap, the world can change. A task can be completed. A new task can appear. A recipient can lose access. A preference can change. Another worker can advance the same operation. The queued record may still be durable and validly serialized while the message it would produce is already wrong.
The lesson is simple: queued intent is evidence about an earlier snapshot. It is not timeless authority.
Store the promise, not the presentation
Suppose a service groups several outstanding items into one reminder. The tempting design is to serialize the titles, amounts, names, and finished message into an outbox row. Delivery is then easy: deserialize and send.
That convenience turns the row into a stale cache of user-facing truth. It may also retain more sensitive data than the queue needs.
A safer durable record can be much smaller. Keep the routing identity, recipient identity, sequence, source, and a deterministic fingerprint of the complete set the message was created for. The fingerprint represents the promise: “this message describes exactly this observed set.”
The display payload is rebuilt later from authoritative state. The durable row says what snapshot was intended without pretending that its old presentation is still current.
Rebuild from the complete current set
At execution time, re-read what the authoritative source currently exposes to the recipient. Then run the same grouping rules again and compare the resulting fingerprint with the stored fingerprint.
Why the complete set? Because stale work changes in two directions.
The obvious case is subtraction. An item was completed or removed after the reminder was queued. The old message now overstates what is outstanding.
The less obvious case is addition. A new item appeared after the reminder was queued. If the worker checks only the item keys remembered by the row, every remembered item may still be valid. The worker can still send a message that understates the current request.
Reconstructing the whole set catches both. If the current fingerprint differs, the old intent is superseded. Stop it. Let the current state produce a new intent under a new identity.
Keep authorization in the authoritative read
The background worker should not invent a second, simplified version of the authorization and status rules used by the foreground feature. That duplicate will drift.
Prefer a shared underlying read path with an explicit recipient parameter. The interactive request and the delayed worker should agree on what the recipient is currently allowed to see and how current status is mapped.
This also makes an important distinction possible. “Nothing remains” is not the same as “the recipient is no longer authorized.” Both may yield an empty visible set, but they mean different things operationally and in an audit trail.
Completion can finish the old intent. Revocation should stop disclosure for a security reason. A missing source integration is different again: it is usually a retryable deployment fault, not evidence that the work is complete.
Model more than send or fail
A robust recheck tends to produce several outcomes:
- Send: the complete current snapshot still matches, preferences allow delivery, and the operation sequence is current.
- Stop as completed: the intended work no longer exists.
- Stop as changed: the set still exists but is no longer the set the queued message promised to describe.
- Stop as revoked: the recipient no longer has authority.
- Stop as superseded: a later sequence has already advanced the operation.
- Defer: authoritative evaluation is temporarily unavailable.
These outcomes can feel verbose compared with a Boolean. That verbosity is useful. It prevents a temporary infrastructure gap from being filed as success, and it prevents a security revocation from being misreported as ordinary completion.
Test addition as carefully as subtraction
The most valuable regression test in this pattern is often the one teams forget: add something after the row is written.
A focused suite should cover an unchanged set, a removed item, an added item, an empty set, revoked authority, changed preferences, a superseded sequence, and an unavailable source. It should also prove that each evaluation performs a fresh authoritative read rather than accidentally reusing stored data.
These tests do more than exercise branches. They define the temporal contract of the workflow: which facts may age, which facts must be refreshed, and which outcome owns each kind of change.
The trade-off is real
This design adds a database read at the most sensitive point, deterministic reconstruction, fingerprint maintenance, and more explicit state transitions. At high volume, that cost deserves measurement. Poorly indexed rechecks can simply move a reliability problem into the database.
The benefit is not that fingerprints make distributed systems magically consistent. They do not. There is still a race between the final read and the external side effect, and systems with stricter requirements may need provider idempotency, transactional boundaries, or domain-specific reservations.
The narrower claim is valuable enough: a delayed worker should not knowingly act on an obsolete snapshot when it can cheaply revalidate current truth.
A practical review checklist
When reviewing delayed email, push, webhook, export, billing, or approval work, ask:
- What exact snapshot did this row promise to act on?
- Does the queue store minimal identity or stale presentation data?
- Can the worker re-read the full authoritative set?
- Are additions detected as well as removals?
- Are completion, revocation, supersession, and temporary unavailability distinct?
- Is the external side effect itself idempotent or otherwise bounded?
Durability keeps work from disappearing. Revalidation keeps durable work from becoming confidently wrong.
Top comments (1)
The additions case is the one that almost never gets a test, so it's good to see it called out. On SQL Server there's a cheap way to build the fingerprint for the trade-off you mention: if the rows carry a rowversion column, SELECT COUNT(*), MAX(RowVer) over the same predicate catches removals through the count and any insert or update through the max, since every write bumps it. That's one indexed aggregate at execution time instead of re-reading the whole set, and the full read only has to happen when the pair doesn't match.