What is the difference between eager and lazy resolution in GraphQL resolvers?
Understand eager vs lazy resolution in GraphQL resolvers, why execution is lazy by default, and how DataLoader batches deferred fetches to avoid over-fetching.
Expected Interview Answer
Eager resolution fetches data up front — often loading a field's value or related records before the engine knows they're needed — while lazy resolution defers work until the engine actually reaches that field in the query, fetching only what the selection set requests.
GraphQL's field-by-field execution is inherently lazy: a nested resolver only runs if the client selected that field, so a resolver that returns a promise or a thunk lets data load on demand. Eager resolution happens when a parent resolver pre-fetches joined data (for example, loading a user's posts inside the user resolver regardless of whether posts were requested). Lazy patterns like DataLoader defer and batch fetches to the tick when fields are resolved, avoiding wasted queries; eager patterns can reduce round trips but risk over-fetching and doing work the client never asked for.
- Lazy resolution fetches only the requested fields, avoiding over-fetching
- Eager resolution can cut round trips when related data is almost always needed
- Lazy patterns pair with batching (DataLoader) to solve N+1
- Choosing per field lets you tune latency vs. wasted work
- Deferring keeps expensive joins off unselected branches
AI Mentor Explanation
Eager resolution is a team that pads up all eleven batters the moment the innings starts, in case they're needed. Lazy resolution sends the next batter in only when a wicket actually falls. GraphQL is lazy by nature: it only 'sends in' a nested resolver when the query truly reaches that field, so no player wastes energy padding up for an over that never comes.
Step-by-Step Explanation
Step 1
Recognize GraphQL's default
Execution is field-by-field and lazy: a nested resolver runs only if its field is in the selection set.
Step 2
Return promises, not pre-fetched data
Have resolvers return promises or thunks so a fetch fires only when the engine reaches and awaits the field.
Step 3
Spot eager over-fetching
Watch for parent resolvers that load joined data (e.g., user.posts) even when the client didn't request it.
Step 4
Introduce batching for lazy fetches
Wrap per-item loads in DataLoader so lazily-resolved siblings are collected and fetched in one batched query.
Step 5
Choose per field deliberately
Eagerly join when related data is nearly always needed; defer when it's expensive and often unselected.
What Interviewer Expects
- Understanding that GraphQL execution is inherently lazy by field
- Ability to explain over-fetching risks of eager parent resolvers
- Knowledge of promises/thunks deferring resolver work
- How DataLoader batches lazily-resolved fields to avoid N+1
- Trade-off reasoning between round trips and wasted work
Common Mistakes
- Eagerly joining related tables in the parent resolver regardless of the selection set
- Assuming returning data directly is always cheaper than returning a promise
- Confusing lazy resolution with caching
- Ignoring the N+1 problem that naive lazy per-item fetches create
- Believing GraphQL always fetches the whole object graph
Best Answer (HR Friendly)
“Eager means grabbing data before you know you need it; lazy means waiting to fetch until the request actually asks for it. GraphQL is naturally lazy — it only runs the code for the exact fields a client requested — which avoids doing wasted work.”
Code Example
// EAGER: parent resolver pre-fetches posts even if they're not requested
const eager = {
Query: {
user: async (_p, { id }, ctx) => {
const user = await ctx.db.users.findById(id);
user.posts = await ctx.db.posts.byAuthor(id); // fetched no matter what
return user;
},
},
};
// LAZY: posts load only if the query selects the posts field
const lazy = {
Query: {
user: (_p, { id }, ctx) => ctx.db.users.findById(id),
},
User: {
// runs ONLY when { user { posts } } is requested; batched via DataLoader
posts: (user, _a, ctx) => ctx.loaders.postsByAuthor.load(user.id),
},
};Follow-up Questions
- How does DataLoader turn many lazy per-item fetches into one batched query?
- When is eager fetching actually the better choice?
- What is the N+1 problem in GraphQL resolvers?
- How does returning a promise from a resolver enable lazy evaluation?
- Can the info argument tell a resolver which nested fields were requested?
MCQ Practice
1. Why is GraphQL execution described as inherently lazy?
The engine only walks fields present in the query, so resolvers for unselected fields never run — resolution is on-demand.
2. What is the main risk of eager resolution in a parent resolver?
Pre-fetching joined data in the parent means loading records even when the query didn't select them — wasted work and over-fetching.
3. Which tool defers and batches lazily-resolved per-item fetches?
DataLoader collects individual .load() calls made during a tick and dispatches them as a single batched, deduplicated fetch.
Flash Cards
Eager resolution? — Fetching a field's (or related) data up front, before the engine knows it's needed.
Lazy resolution? — Deferring a fetch until the engine actually reaches that field in the query.
Is GraphQL lazy by default? — Yes — nested resolvers run only if their field is in the selection set.
How do you make a resolver lazy? — Return a promise or thunk so the fetch fires only when the field is reached and awaited.
Downside of naive lazy per-item fetch? — The N+1 problem — solved by batching with DataLoader.
Continue Learning
Related Interview Questions
What is the N+1 problem in GraphQL and how does DataLoader solve it?
hard
What are resolvers in GraphQL and how do they work?
medium
What is the difference between over-fetching and under-fetching, and how does GraphQL address them?
medium
What is a GraphQL context object and what does it typically contain?
easy