Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background.

Production Architecture with Dmytro Nasyrov

Archives
Software development
About Dmytro Nasyrov
Log in
Subscribe
October 27, 2026

Cache Invalidation: Verify the Reader’s Version

A quiz exit evicted myBaseline before navigation. Trace accepted versus observed data, first render and late responses with a reusable record.

A successful invalidation tells you that a cache entry was removed or made unusable. It does not tell you what the next screen displayed. When a user finishes an action and lands on an older result, the useful question is specific: which accepted state did this reader consume, through which cache key, on its first render?

That question changes the debugging boundary. A passing mutation and a passing eviction call can coexist with a stale destination. Verification has to follow the value through the handoff, including responses already in flight. Use a write followed by an identifiable read as the unit of evidence, with an explicit rule for what the reader may show while freshness remains uncertain.

The website case: a destination that could read too early

In its published optional quiz concurrency case, Pharos Production describes a concrete cache problem in an exit flow. Stas Rysin's October 22 engineering note explains that terminal handling evicted the old myBaseline field and, when available, wrote the accepted response under the relevant profession before calling the completion callback. A destination could read immediately on mount, ahead of a network refresh. Profile and Ready needed consistent terminal data.

The case also limits the guarantee: leaving remained possible when remote cleanup failed, and a late answer could not restore the active screen. The recorded behavior establishes local terminal handling, not exactly-once processing across service failures.

The method below extends that ordering problem into a proposed verification workflow. Its revision numbers, trace fields and test schedules are illustrative. They are not implementation details or measurements reported in the case. The practical objective is to make the first destination read inspectable before a background refresh hides the failure.

Define the read promise before choosing the cache operation

Microsoft's Cache-Aside guidance states:

The Cache-Aside pattern doesn't guarantee consistency between the data store and the cache.

Microsoft, Cache-Aside pattern

That limitation should become an application decision. For an editable profile, the submitting user may expect the accepted value immediately after saving. Another person browsing a public summary may tolerate a bounded delay. A single cache policy can serve both paths only if their different promises remain explicit.

Write the promise in terms the interface can satisfy. For example: after an accepted update, this session displays that accepted result or a later comparable revision; otherwise it displays a visible refreshing state. This is a proposed contract for a particular read path. It says nothing about every replica, every browser or all future reads.

Define what accepted means as well. A queued command, a committed record and a completed projection represent different milestones. If the write endpoint only acknowledges receipt, the next screen cannot honestly label the projected result current merely because that endpoint returned successfully. Carry the pending operation identity until the relevant completion condition can be checked.

A deadline needs a start event and an observable finish. Starting a timer when a button is pressed includes request time. Starting it at confirmed commitment measures a narrower propagation interval. Either can be useful, provided the recorded metric says which one it measures. Without this definition, a fast invalidation graph can conceal a slow or incorrect reader experience.

Give the reader a precise identity

Start with the resource and the view. A cache entry for a person's current assessment and an entry for that person's assessment in a particular profession may contain different data while sharing a familiar field name. Record the arguments that select the value. Include the tenant, user, locale or projection only when they actually affect the response. Distinguish resource identity from operation identity. The resource identifies what the reader wants. The operation identifies the particular change being traced. A request identifier connects logs but does not establish data freshness. An object identifier can remain stable across hundreds of updates. Neither substitutes for a revision or another meaningful acceptance condition.

If the service exposes an ordered revision for this resource, retain it with the accepted response and the observed result. Compare revisions only within their documented ordering domain. Revision 43 for one record is not necessarily newer than revision 42 for another. An opaque validator may support equality without supporting numerical ordering.

When no revision exists, be honest about the weaker evidence. A test can compare a deliberately changed field or a server-issued operation result. That may prove the intended update reached one view, while saying little about intervening updates. Do not invent a timestamp-based ordering rule unless the service contract makes that comparison valid.

Also identify the reader instance. A newly mounted page, an existing sidebar and a second tab can take different paths to the same object. A successful observation in one does not prove another consumed the same result. Keep the scope narrow enough that a reviewer can reproduce the read and understand which additional consumers still need verification.

Use one reader-version propagation record

The following worksheet is a proposed debugging artifact. Its fictional resource is an assessment summary scoped to a profession. Revisions 41 and 42 stand for comparable server revisions within that one resource. The actual website case does not report these fields or values.

Observation Illustrative entry What it establishes
Resource and scope assessment A, profession P Which read belongs to the change
Accepted write result revision 42, terminal state Minimum accepted state for this handoff
Invalidation target summary query for A and P Which cached representation was addressed
Cache value after publication revision 42 Value available to that local reader path
Destination's first observation revision 41 Handoff still displayed a superseded result
Later observation revision 42 after refresh Recovery occurred, but first-read failure remains
Freshness deadline first destination render When this particular promise must hold
Fallback show accepted result or refreshing state Permitted behavior while confirmation is missing

Add event order around the record: write accepted, invalidation applied, result published, navigation callback invoked, destination read and destination render. Capture ordering within a process with an appropriate monotonic clock or sequence number. Cross-service timestamps may help investigation, but unsynchronized clocks cannot establish a reliable order on their own.

Keep acceptance evidence separate from observation evidence. The write response supplies the expected result. Instrumentation at the reader supplies the consumed result. Populating both columns from the mutation response would produce an attractive record that never tests the destination. Likewise, inspecting the cache after the screen has rendered cannot prove what was visible earlier.

For an aggregate view, a single source revision may be insufficient. A dashboard can combine several resources with different update paths. Record the component relevant to this action and the projection's own completion evidence, if available. Avoid turning one row into a claim that the entire dashboard represents a globally consistent snapshot.

Walk the record through one failed handoff

Consider a deliberately constructed reproduction. A summary reader starts while revision 41 is current. Its response is held in the test harness. A finish action then returns accepted revision 42. The application removes the old summary entry and writes revision 42 before opening the destination. At this point, the local cache inspection looks correct.

Now release the held revision 41 response. If the destination shows revision 41, capture the callback or cache write that accepted it. If the cache still contains revision 42 while the screen shows revision 41, inspect the component's own state assignment instead. This single controlled change separates stale cache repopulation from stale local rendering without changing the origin or adding another refresh.

Repeat the same schedule with the acceptance guard enabled. Keep the old response held until after the first destination assertion, then release it and assert again. Both observations matter: the first checks the navigation contract; the second checks that older work cannot undo it. Finally, release the authoritative refresh and confirm that the reader remains in an acceptable state. A fix that passes only after this last step has not demonstrated the first-render promise.

This reproduction is intentionally small. It proposes a way to expose the race, not a report that the website implementation suffered this exact interleaving. Keep its fixtures and expected states with the release evidence so the next change can reuse the same ordering challenge.

Choose invalidation, publication, refresh or a warning

Invalidation is useful when the old representation should no longer be reused. Publishing the accepted result is useful when that response contains enough authoritative data to satisfy the immediate view. Refresh is useful when the destination needs a projection the write response does not supply. A warning is useful when the application cannot currently substantiate its freshness promise. These operations can be combined, but each needs a reason. Removing an obsolete query does not manufacture its replacement. Writing a returned object does not necessarily update every query that references it. Refetching schedules work whose outcome still has to reach the intended reader. Choose the smallest combination that covers the actual view and its dependent consumers.

Apollo's cache interaction documentation distinguishes direct cache writes and field modification from query behavior controlled by fetch policies. Its eviction documentation also describes references that can remain after an object is evicted. For a normalized cache, check both the entity and the query or field that selects it. Do not treat a successful method call as a complete view-level assertion.

Before publishing a mutation result locally, check that it belongs to the current resource scope and still represents an acceptable state. A response for a previously selected profession should not populate the newly selected profession's view. If a later accepted update already exists, an earlier response must not overwrite it simply because it arrived last.

When the response is partial, resist filling missing fields with a label that implies complete freshness. Show the accepted portion with its proper status, or refresh the required representation. The interface should distinguish a confirmed result, a pending projection and an unavailable confirmation. Those states lead users to different decisions and should survive the navigation handoff.

Keep late responses inside the verification boundary

One useful failure schedule starts a read before the write completes. Hold that read's response. Accept the write, invalidate the relevant entry and publish the new result. Then release the old response. If it repopulates the cache or replaces the screen, invalidation succeeded while the freshness contract still failed.

Redis documents a related race in its client-side caching reference: with separate data and invalidation connections, an invalidation can arrive before an older read response. The reference describes a placeholder approach that prevents that response from repopulating an entry after the placeholder has been removed. This is a specific protocol situation, not evidence that every client cache behaves identically.

For an application-level design, a local generation counter can help reject work that started before invalidation. Capture the generation when a request starts and compare it before accepting its result. That counter identifies local lifecycle changes. It does not prove that the server response contains the latest committed data, and it must not be presented as a server revision.

A server revision check can answer a different question: whether a returned representation is older than a comparable accepted version. The two checks may complement each other. One rejects obsolete local work; the other evaluates data ordering. Document which condition rejected a result so an investigation can distinguish a lifecycle race from propagation delay.

Also test a delayed write response. Suppose update B is accepted after update A, but A's response arrives later. A simple last-response-wins assignment can regress the view. The acceptance rule needs to follow the service's ordering guarantees or an explicit operation policy. Where no comparable revision exists, preserve the uncertainty rather than sorting opaque tokens or guessing from arrival time.

Test the first render before the refetch repairs it

A test that awaits every background request before checking the page can miss the entire defect. Arrange the old cached value and hold the refresh response. Perform the action, allow navigation and inspect the destination while that refresh remains pending. The first meaningful assertion concerns the state visible at that moment.

Use distinct fixtures. Make the old and accepted results different in a field the destination actually displays. Keep unrelated values stable so a mismatch points to the intended resource. If both fixtures happen to contain the same status, the wrong source can pass unnoticed. Revision assertions and visible-field assertions should corroborate each other when the test surface exposes both.

Exercise two consumers deliberately. One can be the destination created by the completion callback; another can be an already mounted profile summary. Verify the first view's handoff and the second view's subsequent observation separately. This catches a local write that repairs one screen while leaving a different query backed by an older representation.

The bounded test set should cover an accepted response with refresh delayed, a refresh failure, an old read arriving after invalidation, a changed scope and two accepted writes whose responses arrive out of order. Add a failed cleanup path when leaving is allowed independently of remote completion. Each schedule should identify the expected visible state before releasing the held request.

Assert prohibited transitions as well as the desired destination. After a terminal user action, releasing a late response should not reopen the active flow. After a newer result is accepted, an older response should not replace it. These checks make the failure legible without requiring arbitrary sleeps. They are proposed test designs here; no application test execution or production outcome is claimed.

Locate the layer that supplied the stale value

A stale screen does not identify its cache layer. Trace the reader's actual path: component state, normalized client cache, browser HTTP cache, intermediary response cache, service cache and underlying read source. Include only the layers present in the system. A diagram copied from another architecture can send the investigation toward infrastructure the request never used.

At each boundary, ask which identity selected the representation and what evidence accompanies it. A service can return an older projection even after a client cache miss. Conversely, the network can return the accepted result while local component state continues rendering an earlier copy. Those situations require changes at different boundaries.

HTTP invalidation has its own scope. RFC 9111, section 4.4 explains invalidation after non-error responses to unsafe requests and notes that this does not ensure appropriate responses are invalidated globally. Do not infer that a successful state-changing HTTP request purged every application query or every intermediary representation.

Capture enough response metadata to distinguish paths, without publishing sensitive payloads. A representation validator, application revision and cache-source label can be useful when their meanings are documented. An HTTP Age value measures response age under HTTP caching rules; it does not directly measure how far a business object lags behind its latest accepted update.

Once the stale boundary is identified, keep the remedy there unless evidence requires a broader change. Clearing every cache may make a demonstration pass while concealing an incorrect query key or an older in-flight response. A targeted reproduction should still fail before the fix and pass afterward with the unrelated caches left in their normal states.

Give uncertainty a deadline and a visible outcome

A freshness deadline is a product choice attached to an action. For an immediate confirmation screen, the boundary may be the first render. For an asynchronously generated report, the boundary may be a later completion event. Document the distinction before choosing a timeout. A universal timeout copied across both flows can make one misleading and the other needlessly unavailable. If the deadline is missed, choose behavior that preserves the user's understanding of the action. Use the accepted result when it fully represents the required view. If a projection is still pending, a refreshing state can explain the delay. Displaying the previous result requires a clear stale label when the contract permits that fallback.

Do not retry the write merely because the read looks old. First determine whether commitment is known, pending or unknown. A repeated mutation can create duplicate work while leaving the original read-path defect untouched. A refresh retry should also be bounded and should not silently replace a confirmed newer result with an older response.

Preserve a route out when the action is optional. The user's ability to leave and the service's ability to confirm cleanup can have different availability requirements. Represent the remote result truthfully after the transition. Avoid turning a best-effort cleanup failure into either an invented success or an indefinite trap on a screen the user chose to leave.

Recovery should close the record. When a later authoritative read satisfies the acceptance condition, record that observation and remove the temporary warning where appropriate. Keep the original deadline miss in the diagnostic history. Eventual recovery is useful evidence, but it does not erase a violated first-render promise or make its timing irrelevant.

Measure the reader path without overwhelming the origin

Operational telemetry should preserve the same distinction as the worksheet. Count observed reader mismatches separately from invalidation failures and refresh failures. Measure the interval appropriate to the declared promise. Track how many observations remain unknown because a revision was absent, the reader closed or instrumentation could not identify the resource scope.

Use a bounded sampling strategy for successful reads and retain enough failed traces to diagnose ordering. Prefer resource-scoped identifiers with controlled access over raw profile data. A useful trace needs state transitions and comparable tokens; it usually does not need the contents of every answer or every response body.

Refresh behavior also consumes capacity. AWS's Caching challenges and strategies discusses the operational risks that caches introduce, including behavior when they become unavailable. Before enabling a broad cache bypass, estimate the request load that will move to the origin. Coordinate refreshes for the same resource where the design permits it and keep retry budgets explicit.

A small rollout should observe both correctness and load. Reader mismatches can fall while origin latency rises enough to create a different failure. Check the relevant read path, refresh volume, origin errors and fallback frequency together. Do not declare success from a hit-rate chart or a latency percentile that excludes the users whose refresh never completed.

Review the handoff as part of the architecture

For teams facing a split between an accepted backend result and a stale interface, Pharos Production's backend development services provide a relevant architecture and implementation scope. A useful review request is concrete: bring one write response, the cache selection rules and the destination's first read. That evidence supports a focused investigation without promising a particular latency or consistency result in advance.

The release record should identify the read promise, the accepted state, the first observed state, the late-response rule and the fallback. Attach the deterministic schedule that distinguishes the old behavior from the proposed fix. Record any reader that remains outside the tested scope, especially another tab or an aggregate view with a separate projection.

Repeat this review when the mutation response changes, a new consumer mounts during navigation, query arguments change or an additional cache layer is introduced. Those changes can invalidate the evidence even when the eviction call stays untouched. The release question remains concrete: after this action, did the intended reader observe an acceptable version at the promised boundary?

More insights to read

  • Caching challenges and strategies
  • Skip for Now Is a Concurrency Requirement
  • Client-side caching reference
  • Cache-Aside pattern
  • RFC 9111: HTTP Caching

About the author

Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background.

Dmytro Nasyrov. Photo supplied by the author.

Written by Dmytro Nasyrov PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.

Don't miss what's next. Subscribe to Production Architecture with Dmytro Nasyrov:
Older → Operator Rules: Trace a Shared Default to Its Override
Software development
LinkedIn
Twitter
YouTube
Powered by Buttondown, the easiest way to start and grow your newsletter.