skip to content

When a downstream read model serves the user, what should an automated case assert instead of the write's acknowledgement?

level: middleimportance: must knowfreq 60%

answer

  1. Two paths: one writes, one reads
  2. The request returning is not the outcome
  3. Ask what a user would actually notice
  4. Query the view the product itself queries

basics

~20 s

Assert on the read model the user actually queries — the projected value on the read path — not the acknowledgement the write returned. An accepted write proves only that the request was taken, never that the user-visible view changed.

solid answer

~40 s

Assert on the observable the user actually reads. In a flow where a request is accepted, an event is published, and a separate component rebuilds a queryable view, the acknowledgement proves only that the request was durably accepted — the projection may never be built at all. A case that stops there passes while the feature is broken for every user. Pick the read path the product itself uses: the list the screen renders, the summary a dashboard queries, the record a client fetches. Assert the field a user would notice rather than an internal row that happens to be easy to reach. Where the projection is genuinely internal and has no user-facing surface yet, name the case so the gap is visible and treat that check as supporting evidence rather than acceptance proof.

code

pseudocode · 12 lines
pseudocode
# flow: request accepted -> event published -> read view rebuilt
ack = place_order(cart)
assert ack.accepted                     # proves acceptance only

# assert on what the product actually reads
view = read_path.customer_order_list(customerId)
assert view.contains(orderId)
assert view.order(orderId).total  == expected_total
assert view.order(orderId).status == "PLACED"

# NOT this - the write store is the same component that acked
# assert write_store.orders[orderId].total == expected_total

go deeper

for a junior

Be ready to say what happens after a request is accepted: the answer comes back before the user-facing view is rebuilt. Recall that a successful response and a visible result are two separate things.

for a middle

Explain the split between the write path and the read path, and name the observable you would assert on. An interviewer expects you to reject the acknowledgement as acceptance evidence and to justify the view you chose.

for a senior

Show how you pick the observable in a real flow with several projections: which one a user would report missing, which belong in their own cases, and how you keep the failure message pointing at a single cause.

for a principal

Own the rule the team writes cases by — acceptance evidence lives on the surface the product serves, and anything closer to the write is labelled a supporting check. Be ready to argue the cost when every team answers this differently.

## Two paths, two different claims In a flow with a downstream projection, one user action travels two paths. The **write path** takes the request, validates it, stores it and returns an acknowledgement. The **read path** is a separate queryable view — a projection, a materialised summary, a search-style index — rebuilt by a component that reacts to the write. The user never sees the write path's storage. Everything the product renders comes off the read path. An automated case has to decide which of those two it will accept as evidence that the behaviour works. The acknowledgement is the easiest thing to reach: it comes back in the same call, it is synchronous, and it is already sitting in a variable. It is also the weakest claim available. *Accepted* means the request was durably taken for processing. It does not mean any consumer ran, that the projection was written, that it was written under a key the read path queries, or that the field the screen renders was populated. ## What each candidate target actually proves | Assertion target | Proves | Misses | |---|---|---| | The acknowledgement | The request was accepted | Everything downstream of acceptance | | The row in the write store | The write path persisted it | Whether any projection was built | | The published event | The write path emitted something | Whether a consumer built the view | | A log line from the consumer | Some code ran | Whether the view is queryable and correct | | The read path the product queries | The user-visible outcome exists | Little that matters to the user | The interesting rows are the middle three, because that is where most weak cases sit. A case that asserts on the write store's row is testing the same component that returned the acknowledgement, twice. A case that asserts on the published event is testing the producer. Both stay green while the projection is broken. ## Choosing the observable Pick the observable a **user would report missing**, then reach it the way the product reaches it. - Query the same read interface the screen or the client calls, not an internal table behind it. An internal table can be correct while the query that serves the screen filters the record out. - Assert the fields a user would notice: the item's presence in the list, the total, the status shown. Not an internal surrogate key or a bookkeeping column. - Where several projections are rebuilt from the same write — a list view, a counter, a searchable index — give each its own case. One case per observable keeps the failure message pointing at one cause and stops three unrelated deployments from sharing a red. - Keep the acknowledgement assertion anyway. It is cheap and it separates *the request was rejected* from *the request was accepted and the projection never appeared* in the failure message. It is necessary; it is never sufficient. ## When there is no user-facing surface yet Sometimes the projection is genuinely internal: another service reads it, or the screen that will render it has not shipped. The rule does not change, it moves. Assert on the interface the **consumer of that projection** uses, and name the case so the gap is visible. Treat that check as supporting evidence rather than acceptance proof, and add the user-visible assertion when the surface lands. Reaching around into the store the projection was built from is testing the plumbing rather than the outcome, and it will keep passing when the projection's own query breaks. ## The failure modes this catches 1. **A consumer that never ran.** Nothing subscribed, or the subscription was pointed at the wrong source. Acknowledgement green; view empty. 2. **A projection written under the wrong key.** The record exists, but the read query cannot find it because the projection keyed it by an internal identifier while the read path queries by the customer-facing one. 3. **A field the projection never populates.** The record appears in the list, but the total the screen renders is empty because the mapping dropped it. 4. **A filter that excludes the record.** The read query carries a predicate — visibility, tenancy, soft-delete — that the projection's writer did not account for. 5. **A payload shape the projection cannot handle.** The consumer fails on one field, abandons the record, and the write path notices nothing. Every one of those is invisible from the acknowledgement, and every one of them is what a user experiences as *I placed the order and it is not in my list*. ## The rule in one line Acceptance evidence lives on the surface the product serves. Anything closer to the write is a supporting check, and should be named as one.

  • The projected view has no user-facing surface yet — what do you assert then?
    Assert on the interface the consumer of that projection uses, even if only an internal query serves it today, and name the case so the gap is obvious. Treat the check as supporting evidence rather than acceptance proof, and add the user-visible assertion when the surface lands. Reaching into the store the projection is built from tests the plumbing, not the outcome.
  • Why is asserting on the published event a weaker target than the projected view?
    The event proves the write path emitted something; it does not prove any consumer built a view from it. A projection that fails on the payload, filters the record out, or writes it under the wrong key leaves the event correct and the user's view empty. Assert on the event only when the event itself is the deliverable another team consumes.
  • Which observable do you pick when three different views are rebuilt from the same write?
    Pick the one whose absence a user would report — usually the surface the flow exists to serve — and give the others their own cases at the level they belong to. Bundling three projections into one case makes the failure message ambiguous and couples the case to three deployment schedules. One case, one observable, one reason to fail.

A courier's receipt proves the parcel was collected, not that it reached the shelf the customer browses.

saying these in an interview costs you the question

  • Treats a successful write response as proof the feature works
  • Asserts on the write store's row instead of the projected view
  • Assumes a published event guarantees the projection exists
  • Picks whichever internal table is easiest to query
  • Bundles several downstream views into one assertion
  • Says eventual consistency makes the outcome unassertable