skip to content

A scheduled import changes content outside the app. How should that change reach the app's cached copies, and what must the receiving endpoint guarantee?

level: seniorimportance: should knowfreq 46%

answer

  1. the app never ran that write
  2. push or pull
  3. a public endpoint that destroys work
  4. verify the signature before parsing
  5. the window is still the guarantee

basics

~20 s

The app never ran that write, so it needs an ingress: an endpoint the external system calls, naming what changed, which maps that onto labels or paths and purges. Authenticate it, validate it, keep a window.

solid answer

~40 s

When the change happens in an editorial system or a scheduled import, no request in the app ever runs the write, so nothing in the app can notice. Two shapes exist: the source calls you, or you go and look. The push shape is a small endpoint that accepts a payload describing what changed and translates it into the app's own invalidation vocabulary — labels for the affected data, or route paths where that fits. Because it is a public POST target that can empty a cache, verify a signature **before** doing work, treat every field as untrusted rather than purging whatever path string arrives, and make repeated delivery harmless. Answer quickly and fan out asynchronously. And keep a window underneath: a signal that never arrives is indistinguishable from no change.

go deeper

for a junior

Know that when content changes in another system, the application runs no code and therefore notices nothing; something has to call in, or the app has to go and look.

for a middle

Explain the endpoint's job as translation — an external event id or collection name mapped onto the labels your own reads recorded — and why unknown events should do nothing rather than flush.

for a senior

Treat it as a public mutation endpoint: signature verified before any work, payload validated rather than obeyed, idempotent under retries, fast acknowledgement with asynchronous fan-out, and every purge logged.

for a principal

Decide who owns the contract between the two systems, what the maximum tolerable propagation delay is, and how silence is detected — the failure here is organisational, since the team that stops sending events is rarely the team that gets paged.

## Why an external change is a different problem When a user submits a form, the write and the invalidation live in the same request, so the code that changed the data can say what it changed. When content is edited in a separate editorial system, or rewritten overnight by a scheduled import, **no code in the application runs at all**. The data underneath every stored copy simply becomes different, and the app has no event to hang an invalidation on. There are only two ways to close that gap: 1. **Push** — the external system notifies the application when something changes. 2. **Pull** — the application checks periodically, comparing a change feed, a modified timestamp or a content hash, and invalidates what moved. Push is timelier and is what editorial workflows expect; pull is the fallback when the source cannot call out, or when you refuse to expose an ingress. Many systems run both: push for latency, a slow sweep for the messages that went missing. ## The ingress is a public mutation endpoint The push shape is a route that accepts a payload, translates it into the application's invalidation vocabulary, and purges. Everything uncomfortable about it follows from one fact: **it is reachable by anyone on the internet and its effect is to destroy cached work.** - **Authenticate before doing work.** A shared secret, or better a signature over the raw body that you verify first. Reject with `401` before parsing, before database lookups, before fan-out. - **Treat the payload as untrusted input.** A body that names a path or a label is an instruction from outside your trust boundary. Validate it against the set of names your application actually uses rather than passing it through; an attacker who can choose the purge scope can flush anything they like. - **Rate-limit and bound the scope.** An unauthenticated or over-permissive endpoint is a cheap amplification lever — one small request causing a full cache rebuild is a denial-of-service primitive against your own origin. - **Be idempotent.** Delivery is at-least-once in every serious eventing system, so duplicates are routine. Invalidation is naturally idempotent — purging twice is harmless — but any extra work you bolt on, such as sending notifications or writing audit rows, is not. - **Tolerate reordering.** Two edits to the same item can arrive out of order. Because invalidation removes rather than sets a value, order matters far less than it would for a write, which is a good reason to keep this endpoint purely an invalidator rather than letting it carry new content. - **Answer fast, fan out asynchronously.** Senders time out and retry. If a purge covers thousands of artifacts, acknowledge receipt and do the fan-out behind the response, or you will convert one event into a retry storm. - **Log what was purged, and by whom.** When a cache empties at 03:00, the audit trail is the difference between a five-minute answer and a day of guessing. ## Mapping the payload onto your vocabulary The external system speaks its own language — document ids, collection names, event types. The endpoint's real job is translation into the names your reads actually recorded: | the payload says | the app purges | |---|---| | an existing document changed | the label for that entity | | a document was created or removed | the collection label, because membership moved | | a taxonomy or global setting changed | a deliberately wide label, accepting the cost | | something you do not recognise | nothing — log it and let the window handle it | That last row matters: an unknown event type should be inert, not a full flush, or every new feature in the source system becomes an outage in yours. ## The backstop A signal that is never sent, is dropped in transit, or is rejected by your own authentication looks exactly like a quiet period with no changes. There is no way to distinguish them from inside the application, so the freshness window remains the guarantee and push remains the optimisation. Two cheap habits keep this honest: - alert on **silence** — if the source normally sends events daily and nothing has arrived for a day, that is a signal in itself; - run the pull sweep occasionally even when push works, and log when it finds something push should have delivered. ## The mental model An external-change ingress is not a webhook chore; it is the one place where a stranger can ask your system to throw away its work. Design it like an authenticated public mutation, translate rather than obey, and never let it become the only thing standing between an editor and a stale page.

  • Why is an unauthenticated invalidation endpoint a denial-of-service risk rather than just untidy?
    Because it is an amplifier. One tiny request can throw away work that cost thousands of renders, and the traffic that arrives next falls straight through to the origin. Repeating that in a loop keeps the cache permanently empty, so a caller with no credentials and almost no bandwidth can hold your origin at full load.
  • The external system may deliver the same event several times. What does that require of the handler?
    Purging is naturally idempotent, so repeated invalidation is harmless. The risk is anything else the handler does — notifying a channel, writing an audit row, triggering downstream jobs. Either keep the endpoint to invalidation only, or deduplicate on an event id so the side effects run once while the purge may safely run again.
  • How would you notice that the source has silently stopped sending events?
    Monitor for silence rather than for errors. Record the time of the last accepted event and alert when it exceeds the normal interval, because a source that has stopped calling looks identical to a source with nothing to say. A periodic pull sweep that compares timestamps or hashes confirms it and repairs the gap meanwhile.

saying these in an interview costs you the question

  • Exposes the invalidation endpoint with no authentication
  • Purges whatever path string arrives in the request body
  • Assumes each external event is delivered exactly once
  • Does the whole fan-out before answering the sender
  • Treats an unrecognised event as a reason to flush everything
  • Drops the freshness window because the signal exists