78 Caching Query and Result Resolver
78 Caching Query and Result Resolver: Efficiency, Best Practices, and Real-World Implementation
By: [Your Name], Senior Software Engineer
Caching is one of the most effective strategies for improving system performance, especially in today’s increasingly real-time and high-traffic world. In this article, I’ll go into detail about query caching and result resolver caching: how they differ, when and how to implement them, and I’ll share best practices based on my experience across several scale-up projects. I’ll also include code examples, real-world simulations, and visualizations of the process using mermaid flow diagrams.
Understanding the Basics: Query Caching vs Result Resolver
Before diving into code and simulations, it’s crucial to understand the difference between the two:
1. Query Caching
Query caching stores the result of executing a specific query against a data source (usually a database). So when an identical query is run again, the cached result is returned without needing to access the database.
2. Result Resolver Caching
Result resolver caching is typically used at the business logic layer or within a data-fetching framework (such as GraphQL or DataLoader). Resolver caching optimizes data fetching across components or fields, avoiding repeated calls to the data source for the same entity.
Benefits of Both Caching Approaches
| Query Caching | Result Resolver Caching | |
|---|---|---|
| Advantage | Reduces DB load, fast response time | Reduces duplicate fetches for the same data |
| Scale | Often used at the DB/server level | Framework/Business Logic Layer |
| Drawback | Prone to stale data when a table changes | Requires managing dependencies between fields |
Case Illustration: An Article Recommendation System
Imagine an article recommendation system with a query like:
1SELECT * FROM articles WHERE published = true ORDER BY recommended_score DESC LIMIT 10; 1{
2 articles(recommended: true) {
3 id
4 title
5 author {
6 id
7 name
8 }
9 }
10}Here:
- Query caching stores the result of the entire article list.
- Result resolver caching comes into play when the
authorfield is called repeatedly.
Simulation Without Caching: The Author Bottleneck
Suppose we fetch 10 recommended articles. Each article fetches its author data. Without a resolver cache, that means 10 queries to the authors table.
Illustrative pseudo-code (without cache):
1const articles = await db.query('SELECT * FROM articles WHERE ...');
2const results = [];
3for (const article of articles) {
4 const author = await db.query('SELECT * FROM authors WHERE id = ?', [article.author_id]);
5 results.push({ ...article, author });
6}
7// Total = 1 (articles) + 10 (authors) = 11 queries
Simulation With Result Resolver Caching
With result resolver caching (for example, using DataLoader ), the system collects all unique IDs and then fetches the authors just once.
Pseudo-code with a resolver cache:
1const articles = await db.query('SELECT * FROM articles WHERE ...');
2const authorIds = articles.map(a => a.author_id);
3const authors = await loadAuthorsByIds(authorIds); // batch
4const results = articles.map(article => ({
5 ...article,
6 author: authors[article.author_id],
7}));
8// Total queries: 1 (articles) + 1 (all unique authors)
Simulation of Query Caching at the Database
What if this heavy recommended-articles query is called frequently (for example, on every homepage visit)? Enable query caching.
Query Caching flow diagram (Mermaid):
flowchart LR
A[Client Request] --> B[Check Cache Layer]
B -->|Cache Hit| C[Return Cached Result]
B -->|Cache Miss| D[Process to DB]
D --> E[Store in Cache]
E --> C
Implementation: Query Caching (Node.js + Redis)
Let’s say the stack uses Express, PostgreSQL, and Redis as the cache.
A simple caching middleware:
1const redis = require('redis').createClient();
2
3async function cacheMiddleware(req, res, next) {
4 const cacheKey = "homepage:articles";
5 const cachedResult = await redis.get(cacheKey);
6 if (cachedResult) {
7 return res.json(JSON.parse(cachedResult));
8 }
9 // Store on res.locals so the downstream handler can use it
10 res.locals.cacheKey = cacheKey;
11 next();
12}
13
14// Main handler
15app.get('/api/homepage', cacheMiddleware, async (req, res) => {
16 const articles = await db.query("SELECT ..."); // Heavy query
17 await redis.set(res.locals.cacheKey, JSON.stringify(articles), 'EX', 60); // TTL 1 minute
18 return res.json(articles);
19});Advantages:
- Only one heavy query per 1 minute / cache TTL.
- Drastically reduces the load on the database.
Implementation: Result Resolver Caching (Node.js + DataLoader)
DataLoader is extremely popular for GraphQL and REST mediation.
1const DataLoader = require('dataloader');
2
3const authorLoader = new DataLoader(async (authorIds) => {
4 const rows = await db.query('SELECT * FROM authors WHERE id = ANY($1)', [authorIds]);
5 // Build a map of id => author
6 const map = new Map(rows.map(row => [row.id, row]));
7 return authorIds.map(id => map.get(id));
8});
9
10// Inside the resolver/handler
11const articleIds = [...]; // result of the article query
12const authors = await authorLoader.loadMany(articleIds);Best Scenario:
- A request comes in and fetches all
articlerecords. - DataLoader collects all unique
author_idvalues. - Only 1 query is made to authors.
- The result is cached (per request — if you want it global, you can add a Redis layer on top).
Table: Query Performance Comparison
| Scheme | Number of Queries | Latency (ms, simulated) | Notes |
|---|---|---|---|
| No Caching | 11 | 500 | Fetches author N times |
| Query Caching Only | 1 | 50 | Everything cached |
| Resolver Caching Only | 2 | 120 | 1x articles, 1x authors |
| Both | 1 | 50 | Entire path cached |
Case Study: Cache Invalidation
The hardest part of caching is invalidation. When a new article is added, you need to make sure neither the query cache nor the resolver cache holds stale data.
A simple approach:
- For the query cache, use a short TTL (e.g., 1 minute).
- For the result resolver, invalidate/clear the cache when the subject data (e.g., the author) changes.
Tips from the field:
- For a short-lived resolver cache (per-request), no manual invalidation is needed.
- For a larger query cache, always balance performance against data freshness.
Conclusion
Query and result resolver caching are the secret weapons in modern data-driven application development. By applying these two layers of caching, you can cut latency from hundreds of milliseconds down to tens of milliseconds — even under 10ms for repeated requests — without overloading the database.
Best-practice checklist:
- Enable query caching for heavy queries that are called frequently.
- Implement resolver caching for fields or entities that recur often in batches.
- Always set up cache invalidation or a reasonable TTL.
- Monitor cache hit-rate and latency in real time.
A carefully designed caching structure will bring your system close to 99% reliability and make it 10x more resource-efficient than brute-force querying.
Want to learn more?
Check out the source code and further discussion on Github
, or DM me on Twitter for a consultation on caching architecture in your system.
Thanks for reading!
References
Happy caching, engineers! 🚀