Dev Skills
← Nearby places·Worked solution

Nearby places — a full worked answer

The whole thing, start to finish, in the order you would actually say it. Read it to learn the shape of a strong answer — then go and produce one yourself, because reading is not the same skill as producing.

core
Common trap
Read this second, not first. Reading a good answer feels like learning and only builds recognition. If you have not tried this problem yet, close this page and write your own answer first. It will feel worse and it is the part that works.

The question

Design a service that answers "what is near me?" (restaurants, shops, stations), ranked by distance and quality, filtered by category and whether they are open now.

What kind of problem this is

Location. Two dimensions pretending to be one, plus a very high write rate from things that move.

Writes hugely outnumber reads, positions are stale the moment they land, and the index gives you candidates rather than an answer.

Before anything: where is the slack?

How long the system has before a person notices. Ask this before you draw a single box — it decides more of the design than scale does.

Query: ~200 ms. Place data freshness: hours. Opening hours: minutes.

Two hundred milliseconds to answer, and the underlying data barely changes. That makes this one of the most cacheable problems on the site, and one of the easiest to over-engineer.

The query budget is a normal mobile-app budget: about 200 ms, because the user has tapped a button and is looking at a loading state. Comfortable compared to typeahead, tight compared to a batch job.

The freshness slack is where the design comes from. Restaurants do not move. A place's location, name and category change perhaps once a year. Ratings shift slowly. So almost all the data behind an answer is effectively static, and users could not tell an index rebuilt daily from one rebuilt continuously.

There is one fast-moving field mixed into an otherwise static dataset: whether somewhere is open right now. That changes on a schedule, plus irregular closures. It is tempting to let that one field force the whole system to be dynamic. Resisting that is the main design skill this problem teaches. Compute openness at read time from static opening hours, and keep the index static.

The other dynamic case is live availability: a table free tonight, a bike at a dock. If that is in scope it is a separate fast path, not a reason to make the place index live.

So: Static data plus a hard query budget means aggressively precomputed and cached geographic tiles. The one dynamic field is computed at read time from static data rather than being allowed to invalidate everything.

1

Requirements

about 5 min

Why this stage exists: Because the question is deliberately vague, and every box you draw before this is a guess.

What you are being asked for: State your assumptions, ask one or two questions that change a box, and propose the scope.
What a strong answer sounds like

Assumptions: global, hundreds of millions of places, mobile-first. Reads far outnumber writes: places change rarely and are queried constantly, an extreme ratio even for a read-heavy system. Density is very uneven: a city block can hold hundreds of places, a rural square kilometre none. Users query a small radius, typically a few kilometres.

First question, and it changes the design more than it sounds: is the ranking by distance alone, or by relevance (rating, popularity, personal history)? Distance-only can be answered entirely from a geographic index. Relevance ranking needs place attributes, and possibly per-user signals, in the ranking step, which is a second system. I will assume distance combined with quality, with no per-user personalisation, because that keeps results shareable and therefore cacheable.

Second question: does "open now" have to be exact? If a special closure must be reflected within seconds, the static-index approach needs a dynamic overlay. I will assume regular opening hours are computed at read time from static data, and irregular closures can lag by minutes.

Scope: query by location, radius and category; rank by distance and quality; filter by open now. Out of scope: navigation and routing, reviews and photos, and live availability such as table booking.

The costs named here
No per-user personalisation in ranking → costs: Results are less tailored. In exchange every answer is identical for everyone in the same area, which makes the result set cacheable. No other lever in this design is as large.
Say this
"Assuming no per-user personalisation, so two people standing in the same place get the same list. That is what makes results cacheable, and caching carries most of this design because places rarely change. One question: does 'open now' need to be exact to the second? Because that is the only fast-moving field, and I would rather compute it at read time than let it make my index dynamic."
Common trap
Assuming personalised ranking without saying so. It removes the cacheability that makes this problem easy, and it takes one sentence to check.
What an interviewer is ticking off in this stage
  • Stated the extreme read:write ratio: places change rarely
  • Noted density is wildly uneven and will matter
  • Asked distance-only versus relevance ranking
  • Asked how exact "open now" must be, and framed it as the dynamic-data question
  • Avoided per-user personalisation to keep results cacheable
  • Proposed a scope and excluded routing and reviews
2

Actors and lifecycle

about 6 min

Why this stage exists: Because the happy path is the easy half. The failure branches are where the design actually lives.

What you are being asked for: List the actors — including the system — then write a place's life as a chain, and add the failure branches.
What a strong answer sounds like

Actors: the searching user, the business owner who edits their own listing, third-party data providers, moderators, and the system, which decides which places qualify as candidates, how to rank them, when a listing is stale enough to demote, and how far to widen a search that finds nothing.

A place's life: submitted or imported → validated and deduplicated → indexed → served in results → updated periodically → possibly closed permanently.

Failure branches. At submitted: the same restaurant arrives from two data providers with slightly different names and coordinates, so without deduplication the results show it twice. This is a constant problem with place data, and it needs fuzzy matching on name and proximity. At validated: the coordinates are wrong, putting a shop in the sea, so results near that point are polluted. This needs plausibility checks against the address.

At indexed: the index build fails and new or changed places stop appearing, silently, because a slightly old index looks normal. Alert on index age, not just on job failure. At served: the area has no places within the radius. Widen it progressively rather than returning an empty list, and when there is nothing nearby, say so rather than showing something 40 km away as if it were close.

At served: the area is extremely dense, so the candidate set is thousands of places and ranking becomes expensive. Cap the candidates and shrink the search radius in dense areas: the same problem in the opposite direction, with the opposite fix.

At served: a place is permanently closed but still in the index. This is the failure users resent most: they travel somewhere that no longer exists. So closure reports need a fast path to demote or remove, faster than the normal index cycle.

And one the system owns without anyone watching: a place open 24 hours and a place that closed 10 minutes ago must be distinguishable at read time. That means evaluating the current time in the place's time zone, including daylight-saving changes and hours that run past midnight, which causes far more bugs than it sounds like it should.

Common trap
Ignoring deduplication. Place data usually comes from several sources, and duplicates are one of the most visible quality problems in this kind of product, far more noticeable to users than a slow query.
What an interviewer is ticking off in this stage
  • Named multiple data sources and the system as actors
  • Wrote the place lifecycle as a chain
  • Handled duplicate places from multiple providers
  • Handled invalid coordinates
  • Handled a silent index build failure with an age alert
  • Handled sparse areas by widening, and dense areas by capping
  • Handled permanently closed places with a fast removal path
  • Raised time zones in the open-now calculation

The lifecycle, drawn

Submittedprovider or ownerValidatedsystemIndexedbatch buildServedsearcherUpdatedownerDuplicate from two providersfuzzy match on name and proximity, merge with an unmerge pathCoordinates put a shop in the seaplausibility check against the addressBuild fails silentlyatomic swap keeps the old index; alert on index ageNothing within the radiuswiden progressively, then say nothing is nearbyThousands of candidates downtownadaptive cell precision plus precomputed top-NPlace permanently closedfast overlay path; do not wait for the daily build
A place, from ingestion to being served. Deduplication and the permanently-closed branch are the two that users notice most.
3

Numbers

about 6 min

Why this stage exists: Because one part of this is hard and the rest is easy, and they look identical until you do the arithmetic.

What you are being asked for: Estimate query rate, index size, and the candidate set per query. Then finish "So the hard part here is ___."
What a strong answer sounds like

Places: 200 million globally. Each is a name, coordinates, category, hours, rating and address: call it 500 bytes. That is 100 GB in total, which fits on a single machine's disk and comfortably into memory across a small cluster. The data is smaller than most people expect.

Queries: 50,000 per second at peak. Each examines candidates within a radius, and this is where density bites. In a city centre, a 2 km radius can contain 5,000 places; in a rural area, three. So the candidate set varies by three orders of magnitude for the same query, which means a fixed radius is the wrong design.

Writes: 200 million places changing perhaps once a year each is under 10 writes per second globally. That is almost nothing, and it changes my plan: this is not a write system, so I should stop thinking about write paths and spend the design effort on reads.

Cache effectiveness: queries cluster heavily in populated areas, and a query from anywhere in a city block can return the same result set. Rounding location to a grid cell makes queries from many users identical, which turns an apparently per-user query into a cacheable one. That is what makes 50,000 queries a second cheap.

So the hard part here is uneven density (the same query costs a thousand times more in a city than in the countryside), combined with making an apparently location-unique query cacheable. Not write throughput, not storage, and not the geo maths.

Say this
"Ten writes a second globally and fifty thousand reads. The write path is not a design problem, so I will say so and move on. So the hard part is density: the same two-kilometre query returns three places in a village and five thousand downtown. And if I round the user's position to a grid cell, everyone on that block shares one cached answer."
Common trap
Treating this as a high-write geospatial problem like live location tracking. Places do not move. Ten writes a second means the design should be about reads, and noticing that saves you from building the wrong system.
What an interviewer is ticking off in this stage
  • Estimated total place data and noticed it is small
  • Estimated query rate
  • Identified that candidate set size varies by orders of magnitude with density
  • Calculated the write rate and concluded it is negligible
  • Spotted that rounding location to a grid makes queries cacheable
  • Finished the sentence naming density and cacheability

The numbers, drawn

Place updates~10 / sec globally
Queries~50,000 / sec
Candidates — city centre, 2 km~5,000 places
Candidates — rural, 2 km~3 places

So the hard part is density, not volume. The same query costs a thousand times more downtown than in a village. That is why cell precision has to adapt, and why fixed-radius designs fail at one end or the other.

Ten writes a second, fifty thousand reads. And a candidate set that varies a thousandfold with density.
4

High-level design

about 12 min

Why this stage exists: Because now — and only now — you have a reason for every box you are about to draw.

What you are being asked for: Draw the design. Justify each box, and be specific about how the geographic lookup works.
What a strong answer sounds like

The geographic index: every place is assigned a geohash cell. At 6 characters a cell is about 1.2 km by 0.6 km. Places are stored keyed by cell, so finding candidates is a lookup of the user's cell plus the eight neighbours, because a place 100 metres away can be just over a boundary and missing it is a visible bug. Justified because a normal two-column index on latitude and longitude cannot answer a proximity query efficiently: it can only narrow on one column, which leaves a band around the earth.

Adaptive precision, which is the direct answer to the density number: use a longer geohash (smaller cells) in dense areas and a shorter one in sparse areas, so the candidate count stays roughly constant regardless of where the user is standing. Alternatively, start at high precision and step down until enough candidates are found. Justified explicitly by the thousand-fold variation from Stage 3.

Ranking: candidates are filtered by category and open-now, scored on distance and quality, and the top N returned. Because the index gives candidates rather than an answer, this step exists in every geospatial design and is where the product quality lives.

Open-now is computed at read time from static opening hours plus the place's time zone. This is the key decision: the index does not need to change as places open and close through the day, so a static index serves a field that appears dynamic. Irregular closures are a small overlay checked at read time. Justified by the slack analysis, which says not to make the index live.

Caching, keyed by rounded location plus radius plus category plus a coarse time bucket. Rounding the location to a grid cell is what makes distinct users share a cache entry, and the time bucket (say 15 minutes) is what lets open-now be part of a cached answer without being wrong for long. Justified by the query clustering observed in Stage 3. This box saves more work than any other in the design.

Serving: place data is small enough to hold in memory across a modest fleet, so a query is an in-memory cell lookup plus a scoring pass with no database round trip. Justified by the 100 GB figure.

Index building: a batch pipeline ingests provider feeds and owner edits, deduplicates, validates coordinates, assigns cells, and produces a new index deployed by atomic swap. Runs daily, justified by the ten-writes-per-second figure. Urgent corrections, such as a permanently closed place, go through a small overlay applied at read time, so they do not have to wait for the next build.

Partitioning: by geographic region, so a query touches only the shard covering that area, and a hot region is contained. Justified by blast radius and by the fact that queries are inherently local.

The costs named here
Rounding location for the cache key → costs: Distances are computed from the rounded point, so ordering can be slightly off for someone at the edge of a cell. Barely noticeable at a few hundred metres, and it is what makes the cache work at all.
Adaptive cell precision → costs: More complex index construction and query logic than a fixed grid. Without it, city queries return thousands of candidates and rural queries return none.
Daily index builds plus a correction overlay → costs: Ordinary edits take up to a day to appear. Urgent corrections bypass it, which means two paths to maintain.
Common trap
Storing latitude and longitude as two indexed columns and querying a bounding box. The database narrows on one column and scans a band around the planet. It works in a demo and collapses at scale, and it is the problem the geospatial index exists to fix.
What an interviewer is ticking off in this stage
  • Explained why lat/lng columns do not answer proximity, and chose a geospatial encoding
  • Searched neighbouring cells, not just the user's own
  • Adaptive precision to handle uneven density, justified by the numbers
  • Ranking as a separate step after candidate generation
  • Open-now computed at read time so the index stays static
  • Cache key includes rounded location, radius, category and a time bucket
  • Index held in memory, justified by the size estimate
  • Daily batch build with atomic swap, plus an overlay for urgent corrections
  • Partitioned geographically

The architecture, drawn

atomic swapmissProvidersOwner editsBuild pipelinededupe, validate, cellGeo indexby cell, top-NCorrection overlayclosuresUserResult cacherounded locationSearch svcrank + open now
A daily batch build produces a static index. The one apparently-dynamic field, open now, is computed at read time so the index never has to change.
Writeshash(key)Shard Aa–hShard Bi–pShard Cq–z
the key decides the shard — same key, same box, every time
Places are assigned to cells by their coordinates. A query reads its own cell plus the eight neighbours, because a place 50 metres away can easily be over a boundary.
5

Deep dive and tradeoffs

about 14 min

Why this stage exists: Because naming what a decision costs is the single clearest signal of seniority there is.

What you are being asked for: Go deep on the two hardest parts. Name a cost after every decision.
What a strong answer sounds like

Hard part one: density, in detail. In a dense city a 2 km radius contains thousands of candidates, so scoring them all per query is the cost driver. Adaptive precision reduces the candidate set, but the better move is to precompute per cell: for each geohash cell and each category, store the top N places by quality, so a dense-area query reads a short precomputed list instead of scoring thousands. Justified by the near-zero write rate: precomputing is cheap when the data barely changes. Cost: many more index entries (one list per cell per category), and because each list is ranked within one cell, a good place just over the boundary can be missed. That is why neighbour cells still get merged in.

The boundary problem, in detail because it is the classic error in this design: a place 50 metres away but in the next cell can share only a short prefix with the user's cell, or none at all across a major boundary such as the equator or the prime meridian. So a prefix match alone misses it; querying the eight neighbours covers it. Where the search radius is larger than one cell, as in a sparse area, step down to a shorter prefix (larger cells) and widen. Cost: more cells read per query, and the candidate set grows, which is the tension adaptive precision is managing.

Hard part two: hot areas. A stadium, a festival, a transport hub at rush hour means one cell receives an enormous share of queries. Because results are cacheable and identical for everyone there, the cache absorbs most of it. That is the payoff for the Stage 1 decision to avoid personalisation. But one cache key being extremely hot makes one cache node hot, so I would replicate hot keys across several nodes with a suffix and read a random one. Cost: N times the memory for those keys and a fanned-out invalidation, both small for a handful of keys.

Pre-warming for known events is worth mentioning, because unlike most spikes these are predictable: a stadium's location and event time are both known in advance.

Deduplication, revisited as an ongoing system rather than a one-off: matching places across providers by name similarity, proximity and category, with a confidence threshold, and merging above it. Two failure directions: merging two different shops in the same building, and failing to merge one shop listed twice. The second is more visible to users, so I would tune toward merging and provide an unmerge path. Cost: some false merges, which need a correction mechanism and a human in the loop.

Consistency per feature: place data is eventually consistent by up to a day, which is fine because places rarely change. Permanent closures are near-real-time through the overlay, because showing a closed business is the most damaging error. Open-now is computed at read time, so it is correct within the cache time bucket. Ratings are eventually consistent by hours. Four features, four different freshness answers.

Multi-region: the index is small enough to replicate in full to every region, so every query is served locally with no cross-region call. Cost: full replication of 100 GB per region, which is cheap and buys local latency everywhere.

What I would monitor: query latency at p99 split by area density, since a global p99 hides the fact that city queries are the slow ones; cache hit rate; index age; the count of results returned per query, where a sudden rise in empty results is the earliest sign a build went wrong; and closure-report processing lag.

The costs named here
Precomputed top-N per cell → costs: A much larger index, and a good place just over a boundary can be missed if neighbours are not merged in. Turns a dense-area query from scoring thousands into reading a short list.
Tuning deduplication toward merging → costs: Occasionally merges two different businesses. Chosen because a visible duplicate is the more common and more noticeable failure.
Full index replication per region → costs: 100 GB held in every region. Cheap, and it removes every cross-region call from the query path.
Say this
"Because there is no personalisation, everyone standing on the same block gets the same answer, so I round the location to a grid cell and cache the whole result set. Cost: distances are computed from the rounded point, so ordering can be slightly off at a cell edge. That decision turns fifty thousand apparently-unique location queries a second into a far smaller set of cache keys."
Common trap
Building this as if places moved. The write rate is ten a second globally, so the right instinct is to precompute aggressively and cache everything. A design that treats this like a live tracking system has misread its own numbers.
What an interviewer is ticking off in this stage
  • Precomputed top-N per cell per category, justified by the low write rate
  • Explained the boundary problem and neighbour-cell merging
  • Stepping down precision in sparse areas
  • Handled hot areas with key replication, and connected it to the no-personalisation decision
  • Mentioned pre-warming for predictable events
  • Deduplication as an ongoing system with a tuning direction and an unmerge path
  • Per-feature consistency answer with four different freshness levels
  • Full index replication per region, with the cost named
  • Monitoring split by density, not just globally
  • Cost named after every decision

The decision this turns on

A
Geohash cells (fixed grid)
  • —Simple: a string prefix, so an ordinary B-tree index answers proximity.
  • —Nearby things usually share a prefix, so each cell lookup is a range scan.
  • —Boundaries are arbitrary, so you must always query the eight neighbours.
  • —One fixed cell size is wrong somewhere: too big downtown, too small in the countryside.
B
Quadtree (adaptive)
  • —Cells subdivide where density is high, so candidate counts stay even.
  • —Handles the city-versus-countryside problem natively.
  • —A tree to build and maintain, rather than a string you compute.
  • —More work when points move, which places rarely do.
Which one: For places, geohash with adaptive precision gets you most of the quadtree's benefit for much less machinery, and the near-zero write rate means rebuilding the grid is cheap. Choose the quadtree when density varies extremely and you need the structure to handle it for you. For moving objects, use neither: keep positions in memory keyed by cell and update them continuously.
How you make the geographic lookup indexable.

And then they push back

A complete answer is where the interview starts, not where it ends. These are the questions this design invites — the interview is decided here far more often than in the diagram.

One key gets 90% of the traffic. What breaks, and what do you do?
A weak answer

"Consistent hashing spreads the load evenly." It spreads keys evenly, not traffic. All requests for one key go to one place no matter how the keys are hashed. This answer confuses two different problems.

A strong answer

"Whichever node owns that key is saturated while the rest idle, and no partitioning scheme fixes it because partitioning distributes keys, not requests for a single key. Three options. Replicate the hot key across N nodes with a suffix and read a random one. It costs N times the memory and a fanned-out invalidation, and it is the right answer for a read-hot key. For a write-hot key, shard the counter and sum on read, accepting that the total is now a computed value. Or handle it on a separate path: an in-process cache on every server absorbs a read-hot key almost completely. I would also detect hot keys automatically with a count-min sketch rather than maintaining a manual list, because yesterday's hot key is not today's."

The cache just died. What happens?
A weak answer

"Requests would go to the database instead, so it would be a bit slower." At the ratios that justify a cache, the database cannot take that load. This answer means you added a cache without checking what it was protecting you from.

A strong answer

"Every read that was being served from memory lands on the database at once. My cache was absorbing something like 99% of reads, so the database sees roughly 100x its normal load, and it will not survive that. A naive design goes down. What I would do about it, designed in advance: coalesce misses so a thousand simultaneous requests for the same key produce one database read rather than a thousand; keep a small in-process cache on each server as a second layer so the hottest keys never leave the machine; shed load at the edge rather than let the database die, because a degraded site beats a dead one; and bring the cache back warm rather than empty, because restarting into an empty cache reproduces the outage. Cost of the in-process layer: servers can briefly disagree with each other."

Why this database and not Postgres?
A weak answer

"It scales better and is designed for high throughput." Vague and unfalsifiable. Postgres handles far more than most people assume, and an answer with no number in it sounds like a preference.

A strong answer

"The answer has to be a number or an access pattern. Something like: the write rate is around 80,000 a second sustained on one logical table, append-only, with no cross-row transactions. That is more than I would plan to commit on one Postgres primary, so I would be sharding it myself and hand-building rebalancing and failover that a distributed store already has. The read pattern is a single-key lookup, so I am giving up joins I do not need. And then what it costs: no cross-partition transactions, so any invariant spanning partitions moves into application code, and analytics needs a separate copy of the data. If I cannot produce a sentence like that, then the correct answer is that Postgres is fine. Saying so is the stronger answer: choosing the boring option on purpose shows more judgement than reaching for the distributed one."

One user reports a slow request. How do you debug it?
A weak answer

"I would check the logs." Which logs, for what? At scale, "check the logs" without a way to find that specific request is a way of saying you have not thought about observability.

A strong answer

"Work down rather than guessing. First: is it slow for everyone or just them? A dashboard of that endpoint's p50 and p99 answers it in seconds — if p99 is bad and p50 is fine, it is probably data-dependent, meaning that user has far more rows than the median. Second: get a trace of their actual request, which requires request ids propagated through every service, and see whether the time went to one slow call or two hundred fast ones. Two hundred fast ones is an N+1, which is invisible in development and common in production. Third: if it is one slow query, run EXPLAIN with their parameters, not typical ones, because the plan can differ once their row count crosses a threshold. The common outcome is that they are a heavy account and the query has no index for their filter combination. What makes this possible at all is having decided beforehand to propagate request ids and to log with enough structure to find one user's request among billions."

Now make it multi-region.
A weak answer

"We would replicate the database to another region." That covers reads. The question is what happens to writes, and that is where the design actually changes.

A strong answer

"Reads are the easy half: replicas in each region, users read locally, and they may be slightly stale — I would say how stale and which reads cannot tolerate it. Writes are the hard part, and there are three options. Single write region: simplest, no conflicts, and users far away pay 150 ms per write, which is fine if writes are rare and unacceptable if they are not. A home region per data partition, so each user's data lives near them: local writes without conflicts, but it needs a routing layer plus a plan for users who move. Multi-leader with conflict resolution: local writes everywhere, and now I own a conflict problem, which is only acceptable where merges are safe. I would pick the second for user-owned data and single-region for anything globally contested like inventory. What changes elsewhere: cross-region replication cost, a failover story per region, and data residency rules that may force certain data to stay in certain places. I would ask about that constraint rather than discover it later."

10x traffic tomorrow. What fails first?
A weak answer

"We would add more servers and scale horizontally." This says nothing. Every system has a first thing to break, and not naming it means you do not know your own design well enough to have found it.

A strong answer

Name a specific component and the reason. "The stateless services scale out fine, so they are not the constraint. The database primary is, because every write goes through one machine and I am at roughly 40% of its write capacity now. So at 4x I am at the ceiling, and I would see write latency climb and connection pool waits before anything errors. The second thing is the cache, because 10x reads on a fixed working set is fine but 10x on a growing key space means my hit rate drops and the misses land on the database that is already the constraint. In order: database writes, then cache hit rate, then the connection pool. And I would find out from p99 write latency, not from errors."

What does this cost per month?
A weak answer

"I am not sure, it depends on the provider." Interviewers rarely want exact prices. They want to know whether you understand which line item dominates, and that is answerable from your own estimates.

A strong answer

"I would not guess at prices, but I can tell you the shape, which is what matters. The dominant cost here is egress bandwidth: my estimate said 1,350 GB a second to viewers, and at that volume nothing else on the bill comes close. Second is storage, because I keep raw data for two years and it grows every day whether anyone reads it or not. Third is compute, which is elastic and therefore the easiest to control. That ordering tells me where to spend engineering effort: a percentage point of CDN hit rate is worth more than any amount of compute optimisation, so cache hit rate is the number I would put on a dashboard and defend. Storage cost I would attack with lifecycle policies moving cold data to cheaper tiers and with tiered granularity. Compute I would run on interruptible capacity because the work has hours of slack."

Replica lag is 30 seconds and a user reads their own write. What do they see?
A weak answer

"They might see slightly old data." They see their own comment missing, which they will report as data loss. Understating this suggests you have not watched users hit it.

A strong answer

"They see the state from before their write, and they will report it as the app losing their work. This is the read-your-own-writes problem, and users notice it because it is their own data. The fix is not to make everything strongly consistent, which would slow every read for everyone. It is to route that user's reads to the leader for a short window after they write, or to have the client pass the write position it saw and let the read wait for a replica that has caught up. Cheapest of all for this specific case: render their own item from what the client already has and do not re-fetch. Cost of leader-pinning: my most active users generate the most leader load, which is a real but bounded price. And separately, 30 seconds of lag is itself a symptom — usually a long transaction or a bulk write hogging the leader — so I would alert on it rather than only design around it."

The topics this answer used

Now do it yourself

You have read a good answer. That is worth very little until you can produce one on a blank page — so go and write this problem without looking, and compare afterwards.