<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/2-30-days-system-design-questions--bt4wxcddi" -->

---
title: 2/30 Days System Design Questions! | daily.dev
description: A classic N+1 query problem is presented: a /orders endpoint fires 51 DB queries per request due to ORM lazy-loading customer data. Four solutions are compared...
canonical: https://daily.dev/posts/2-30-days-system-design-questions--bt4wxcddi
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: 2/30 Days System Design Questions! | daily.dev
og:description: A classic N+1 query problem is presented: a /orders endpoint fires 51 DB queries per request due to ORM lazy-loading customer data. Four solutions are compared...
og:url: https://daily.dev/posts/2-30-days-system-design-questions--bt4wxcddi
og:image: https://api.daily.dev/og/posts/bT4wXCdDI.png
og:image:alt: 2/30 Days System Design Questions!
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 2/30 Days System Design Questions!

**[Joud Awad](https://daily.dev/sources/iac4jsbu0lv8wbsc85fsh)** · [@joudawad](https://daily.dev/joudawad) · 2 min read · 275 upvotes · 24 comments

## Summary

A classic N+1 query problem is presented: a /orders endpoint fires 51 DB queries per request due to ORM lazy-loading customer data. Four solutions are compared — eager loading with a JOIN (Prisma include), DataLoader batching, Redis caching, and denormalization — each with different trade-offs for future scalability. The post frames it as a team debate exercise rather than providing a definitive answer.

## Content

Your /orders endpoint loads 50 orders on a page.

P95 is 2.4s. The DB's fine. The app server's fine. Nothing's on fire.

Then you open the query log.

51 queries per request. One SELECT for the orders list. Then 50 more — one per order — to fetch the customer. The ORM is doing lazy-load on order.customer inside your map.

Classic N+1. You've seen it before. The fix is "obvious" — until the team meeting, where three engineers propose three different things and everyone thinks they're right.

Here's what's on the table:

A) Eager-load the relation — include: { customer: true } on the Prisma query. One JOIN, done.

B) Add a DataLoader in front of the customer lookup — batches the 50 IDs into one WHERE id IN (...) behind the scenes.

C) Cache the customer by ID in Redis — every lookup hits cache first, DB only on miss.

D) Denormalize customer_name onto the orders table — read it straight from the orders row, no join, no second query.

All four get the query count down. All four ship to prod in real codebases. But the pattern you pick is a bet on what this endpoint becomes in 6 months, not what it is today.

Pick one — A, B, C, or D — and tell me why. Full breakdown in the comments, including which two answers senior engineers split on in code review (and which one is a career-long argument I still watch play out).

If your team has had this exact fight in Slack, send them this post. The debate is worth more than the post.

Drop your answer 👇

#30DaysOfSystemDesign #SystemDesign #Backend #Databases

## Community discussion

Top comments from developers on daily.dev.

**@joudawad** · 23 upvotes

> **Why A wins:** One LEFT JOIN customers ON orders.customer_id = [customers.id](http://customers.id) returns the whole page in a single round trip. P95 drops from 2.4s to ~80ms. No new infrastructure. No new failure modes. The N+1 problem in an ORM almost always has the same root cause: a lazy relation the developer didn't realize was lazy. Prisma's include, Sequelize's include, TypeORM's relations — every ORM has this. Reach for fancier patterns only when JOIN genuinely can't solve it.
>
> ![ChatGPT Image May 7, 2026, 10_22_24 PM...

**@joudawad** · 11 upvotes

> **Why B is the trap (DataLoader):** Beautiful pattern — but earns its keep in GraphQL where the resolver graph fans out in ways a single JOIN can't express. Here you have one list endpoint with one relation. A JOIN is strictly simpler and strictly faster. You're solving a problem you don't have and paying the complexity tax forever.

**@danbars** · 11 upvotes

> This only works if you have one server with access to both orders and customers. Often with micro services architecture you'll have 2 separate servers, each one with its own DB, and then A isn't possible.
> In such case I'd add a new bulkGet or query endpoint on the customers server that allows fetching by a list of customerIds.
> Also D isn't that bad assuming all you need is the name, especially if you need to allow query orders by customer name. In ecommerce system orders often have a snapshot of the customer name at the time of order, so you don't even have to pay the price of updating...

**@joudawad** · 7 upvotes

> **Why D is the career-long argument (denormalize):** Legitimate at Instagram scale. Also how you end up with a customer's maiden name three years after they got married because nobody wrote the update path. Denormalization is a write-amplification decision masquerading as a read-optimization. Do it when the JOIN genuinely can't hit your latency SLO at your scale — not before.

**@joudawad** · 7 upvotes

> **Why C is wrong (Redis):** Cache-first fixes the symptom, not the query pattern. You still issue 50 cache GETs per request, now own a cache invalidation problem every time a customer updates, and cold-hit every ID after a deploy. Cache is a scaling tool, not an N+1 tool.

---

Tags: [#career](https://daily.dev/tags/career), [#prisma](https://daily.dev/tags/prisma)

[View this post on daily.dev](https://daily.dev/posts/2-30-days-system-design-questions--bt4wxcddi)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"DiscussionForumPosting","mainEntityOfPage":"https://daily.dev/posts/2-30-days-system-design-questions--bt4wxcddi","headline":"2/30 Days System Design Questions!","text":"A classic N+1 query problem is presented: a /orders endpoint fires 51 DB queries per request due to ORM lazy-loading customer data. Four solutions are compared — eager loading with a JOIN (Prisma include), DataLoader batching, Redis caching, and denormalization — each with different trade-offs for future scalability. The post frames it as a team debate exercise rather than providing a definitive answer.","url":"https://daily.dev/posts/2-30-days-system-design-questions--bt4wxcddi","datePublished":"2026-05-07T19:25:30.583Z","dateModified":"2026-05-07T19:25:51.647Z","author":{"@type":"Person","name":"Joud Awad","url":"https://daily.dev/joudawad","image":"https://media.daily.dev/image/upload/s--dOB9RaXY--/f_auto/v1773320801/avatars/avatar_iaC4JsBU0lV8wBsc85fSh?_a=BAMAMiiu0","description":"Principal Solution Architecture ","worksFor":{"@type":"Organization","name":"Metalab","logo":"https://www.google.com/s2/favicons?domain=metalab.com&sz=128"},"interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"EndorseAction"},"userInteractionCount":81900}},"image":"https://media.daily.dev/image/upload/s--yG0DfFcY--/f_auto/v1778181934/posts/bT4wXCdDI?_a=BAMAMiWQ0","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":275},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":24}],"comment":[{"@type":"Comment","text":"Why A wins: One LEFT JOIN customers ON orders.customer_id = customers.id returns the whole page in a single round trip. P95 drops from 2.4s to ~80ms. No new infrastructure. No new failure modes. The N+1 problem in an ORM almost always has the same root cause: a lazy relation the developer didn’t realize was lazy. Prisma’s include, Sequelize’s include, TypeORM’s relations — every ORM has this. Reach for fancier patterns only when JOIN genuinely can’t solve it.","datePublished":"2026-05-07T19:25:57.168Z","url":"https://daily.dev/posts/bT4wXCdDI#c-1AkDH1cfW","author":{"@type":"Person","name":"Joud Awad","url":"https://daily.dev/joudawad","image":"https://media.daily.dev/image/upload/s--dOB9RaXY--/f_auto/v1773320801/avatars/avatar_iaC4JsBU0lV8wBsc85fSh?_a=BAMAMiiu0"},"interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":23}},{"@type":"Comment","text":"Why B is the trap (DataLoader): Beautiful pattern — but earns its keep in GraphQL where the resolver graph fans out in ways a single JOIN can’t express. Here you have one list endpoint with one relation. A JOIN is strictly simpler and strictly faster. You’re solving a problem you don’t have and paying the complexity tax forever.","datePublished":"2026-05-07T19:26:08.486Z","url":"https://daily.dev/posts/bT4wXCdDI#c-BrZAWcG5j","author":{"@type":"Person","name":"Joud Awad","url":"https://daily.dev/joudawad","image":"https://media.daily.dev/image/upload/s--dOB9RaXY--/f_auto/v1773320801/avatars/avatar_iaC4JsBU0lV8wBsc85fSh?_a=BAMAMiiu0"},"interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":11}},{"@type":"Comment","text":"This only works if you have one server with access to both orders and customers. Often with micro services architecture you’ll have 2 separate servers, each one with its own DB, and then A isn’t possible.\nIn such case I’d add a new bulkGet or query endpoint on the customers server that allows fetching by a list of customerIds.\nAlso D isn’t that bad assuming all you need is the name, especially if you need to allow query orders by customer name. In ecommerce system orders often have a snapshot of the customer name at the time of order, so you don’t even have to pay the price of updating orders if the customer’s name is updated.","datePublished":"2026-05-11T20:13:17.782Z","url":"https://daily.dev/posts/bT4wXCdDI#c-XEF04dzKb","author":{"@type":"Person","name":"Dan","url":"https://daily.dev/danbars","image":"https://media.daily.dev/image/upload/s--vHUf-Zac--/f_auto/v1747668990/avatars/avatar_ynaPwm4H5DQg2VfOQGkfX?_a=BAMClqUq0"},"interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":11}},{"@type":"Comment","text":"Why D is the career-long argument (denormalize): Legitimate at Instagram scale. Also how you end up with a customer’s maiden name three years after they got married because nobody wrote the update path. Denormalization is a write-amplification decision masquerading as a read-optimization. Do it when the JOIN genuinely can’t hit your latency SLO at your scale — not before.","datePublished":"2026-05-07T19:26:18.313Z","url":"https://daily.dev/posts/bT4wXCdDI#c-i10QvSSXO","author":{"@type":"Person","name":"Joud Awad","url":"https://daily.dev/joudawad","image":"https://media.daily.dev/image/upload/s--dOB9RaXY--/f_auto/v1773320801/avatars/avatar_iaC4JsBU0lV8wBsc85fSh?_a=BAMAMiiu0"},"interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":7}},{"@type":"Comment","text":"Why C is wrong (Redis): Cache-first fixes the symptom, not the query pattern. You still issue 50 cache GETs per request, now own a cache invalidation problem every time a customer updates, and cold-hit every ID after a deploy. Cache is a scaling tool, not an N+1 tool.","datePublished":"2026-05-07T19:26:13.434Z","url":"https://daily.dev/posts/bT4wXCdDI#c-LuOaRIrrp","author":{"@type":"Person","name":"Joud Awad","url":"https://daily.dev/joudawad","image":"https://media.daily.dev/image/upload/s--dOB9RaXY--/f_auto/v1773320801/avatars/avatar_iaC4JsBU0lV8wBsc85fSh?_a=BAMAMiiu0"},"interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":7}}],"isPartOf":{"@type":"WebPage","url":"https://daily.dev/sources/iac4jsbu0lv8wbsc85fsh","name":"Joud Awad"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Joud Awad","item":"https://daily.dev/sources/iac4jsbu0lv8wbsc85fsh"},{"@type":"ListItem","position":3,"name":"2/30 Days System Design Questions!"}]}
```

