Skip to content
Preptima
hardDesignMidSeniorStaffLead

One screen needs data owned by three different services, and it has to render in 200 milliseconds with a filter and a sort applied. How does that request get served?

Fanning out in parallel is fine while every service can answer independently, but a filter or sort spanning owners cannot be composed at all. Either make one service the driver and hydrate its page of identifiers, or build a read model from events and pay for staleness deliberately.

6 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Does the candidate notice that a filter or sort crossing service boundaries cannot be answered by fan-out, regardless of how fast each call is
  • Whether composite latency is reasoned about as the slowest call plus the tail effect of taking a maximum
  • That the availability of the composite is derived from its dependencies rather than assumed equal to theirs
  • Can they state a partial-failure policy field by field, instead of failing the whole screen on any error
  • Whether the staleness of a read model is bounded, measured and made visible rather than hoped to be small

Answer

Start by asking what the screen is actually querying

The answer depends almost entirely on one distinction that candidates rush past. If the screen shows a set of things chosen by one service and merely decorated with facts from the others, composition works. If the choosing itself spans owners — filter on a field held by service A, sort on a field held by service B, then take page three — composition cannot answer it, and no amount of parallelism or caching changes that.

So establish which you are dealing with before designing anything. "Show these twenty orders with the customer's name and the shipment's status" is decoration. "Show orders from customers in Germany sorted by shipment date" is a distributed query, and a distributed query over independently owned datastores is the problem this whole section of microservices practice exists to work around.

Composition, and the two numbers it changes

Take the decoration case first. A composition layer, usually a gateway or a backend-for-frontend owned by the team that owns the screen, calls the three services and assembles the response. Call them concurrently rather than in sequence, because sequential calls sum and concurrent calls do not, and give each one a timeout drawn from the remaining budget rather than a fixed value.

Two numbers get worse and both should be derived out loud. Latency becomes the slowest of the three plus assembly and network overhead, so if each service answers in 60ms at p50 and 180ms at p99, your p50 is a little over 60ms and your budget is intact, but your tail is not: the composite is slow whenever any call is slow, so with three independent calls the chance of hitting a p99-class response is close to three times the chance for one call. A screen with a 200ms target and three dependencies at 180ms p99 is missing that target several percent of the time, and you cannot fix it by making the median faster.

Availability multiplies the same way. Three dependencies each up 99.9 percent of the time give a composite around 99.7 percent if every one of them is required, which is roughly a doubling of your downtime for a service that added no logic of its own. That arithmetic is the argument for the next paragraph.

Decide, per field, what the screen does without

The composition layer must have an explicit policy for each dependency, and the useful framing is which parts of the screen are load-bearing. The order list itself has no substitute, so if that service is down the screen fails honestly. The customer's display name is decoration: on failure, render the identifier, or a cached name from the last time you saw one, and continue. The shipment status might sit in between, in which case the answer is to render the row with that column marked as unavailable rather than empty, because an empty column is a claim about the world and an unavailable one is not.

Making that policy explicit has a second benefit: it tells you what to instrument. A composition layer should report, per dependency, how often it timed out and how often the response was served degraded, because otherwise a permanently broken decoration call looks like a healthy screen right up until someone notices the names have all been identifiers for a month.

Cross-owner filtering and sorting has only two honest answers

Now the hard case. A filter and a sort spanning owners means the result set cannot be determined without data from more than one service, and pagination makes it worse, because you cannot know which twenty rows belong on page three without ranking all candidates first. Fetching everything from each service and joining in the composition layer works at demo scale and collapses at production scale, and it collapses silently, because the failure mode is memory and latency that grow with data volume rather than with traffic.

The first honest answer is to make one service the driver. Choose the service whose data determines the result set, push the filter and the sort into it, let it return a page of identifiers, and hydrate that page from the others. This works whenever the selecting fields all belong to one owner, and the design work is checking that they do and pushing back on requirements that break it. Adding a filter on a field owned by a different service is not a small change to this design, it is a change of design, and saying so early is what stops the composition layer from quietly growing an in-memory join.

The second is a read model. Consume the events the three services already publish, build a denormalised projection shaped exactly like this screen, and serve the request from a single query against it. Filtering, sorting and pagination all become ordinary, the latency becomes one datastore round trip, and the availability of the screen no longer depends on all three services being up at the moment of the request.

flowchart LR
  A[Orders service] --> E[Event stream]
  B[Customers service] --> E
  C[Shipments service] --> E
  E --> P[Projection builder]
  P --> V[Screen read model]
  V --> Q[Query with filter sort page]

The component to interrogate is the projection builder, because it is the one that has no owner by default and the one whose lag becomes your correctness bound.

What the read model costs, stated honestly

Staleness is the headline cost, and it must be bounded rather than assumed. Measure the lag of the projection as an explicit metric, alert on it, and decide what the screen does when the lag is high: serve stale data with a timestamp, or degrade. The user-visible edge is the person who has just made a change and does not see it, and the standard remedy is to route that one user's read to the owning service, or to overlay their own recent write on the projection, rather than trying to make the pipeline synchronous.

Rebuild capability is the cost people forget. A projection with a bug produces wrong data that is not obviously wrong, so you need to be able to rebuild it from the retained event history into a new copy and swap it in, with the old one still serving while the new one catches up. If your events are not retained long enough to do that, the read model is not rebuildable and you have created a second source of truth by accident.

Then ownership and duplication. The projection duplicates data whose meaning belongs to three other teams, so a change in what "shipment status" means is now a change in two places, and the team owning the screen must be a real consumer of those events with a real stake in their contract.

The option that looks cheapest and is not

The tempting shortcut is to give the screen read access to the other services' tables, or a replica of them. It is fast to build, it makes the join trivial, and it removes the boundary you were trying to maintain. The three owning teams can no longer change their schemas without breaking you, which means in practice they stop changing them, and the cost surfaces months later as three services that cannot evolve rather than as one screen that is slow.

If the answer to this question is genuinely that these three datasets are queried together constantly, with filters and sorts crossing all of them, that is evidence about your boundaries rather than a requirement for a clever read path. The defensible response may be that these should not have been three services, and offering that possibility is usually the strongest thing you can say in this round.

Likely follow-ups

  • Your read model is nine seconds behind and a user has just made a change they expect to see. What do you do for that one user?
  • The pipeline that builds the view had a bug for two days. How do you rebuild it, and can you do that while serving traffic?
  • Who owns the composition layer, and what happens to it when a fourth service joins the screen?
  • One of the three services wants to change its schema. Which of your two designs makes that harder for them?

Related questions

Further reading

microservicesapi-compositioncqrsread-modelpartial-failure