r.dishes looks like a field. It’s a database round trip.
ORMs lazy-load an association on first touch. Right for one object, wrong inside a loop over fifty.
Fix it with EAGER?
Bad: Mark the relation EAGER
It loads everywhere, and JPA still runs one select per row without JOIN FETCH.
Good: Fetch per use case
Each page loads what it needs, in bulk.
≈ 2 s
for 51 lazy queries across regions
Assume 40 ms per round trip from Singapore to Mumbai. The same page is 25.5 ms in one zone; batching makes it ≈ 80 ms.
JOIN two collections and 50 cards become 60,000 rows
30 dishes × 40 reviews per restaurant. JOIN to-one relations and at most one collection; load the rest with a second query.
The fix: one trip with a basket
- Load the 50 restaurants
- Collect their ids
- Fetch every dish WHERE restaurant_id = ANY($1)
- Group the dishes by restaurant in memory
1,000 page views a second, and a pool of 20 connections that looked generous.
The catch
By Little’s law, 25.5 ms per request holds ≈ 26 connections. Requests queue for the pool; batched, they hold ≈ 2.
Parallel calls hide N+1 between services. They don’t fix it.
Assume 3 ms a call. Ten at a time cuts 150 ms to 15, but the user service still gets 50 requests. Batch them.
Remember
Count queries per request, then load in bulk. JOIN to-ones, one ANY query per collection, a batch endpoint between services.