Hey, I'm Marco and welcome to my newsletter!
As a software engineer, I created this newsletter to share my first-hand knowledge of the development world. Each topic we will explore will provide valuable insights, with the goal of inspiring and helping all of you on your journey.
In this episode I show you how to introduce a caching system like Redis by showing you how I did it for the Magic Link-based authentication app.
You can download all the code shown directly from my Github repository: https://github.com/marcomoauro/magic-link-auth
👋 Introduction
Let us imagine that we have an API that aims to retrieve information from the database and then serve it up on a web page by the client.
Managing numerous database calls that repeatedly return very similar data can cause inefficiency problems because the database must be queried each time, consuming time and resources to obtain the same data over and over again.
Employing a cache can significantly alleviate this problem.
By caching frequently accessed data, subsequent requests for the same data can be served directly from the cache, reducing the need to query the database repeatedly. This speeds up response times and reduces the load on the database server, resulting in improved overall system performance.
What if the data are changed?
Cache introduces a trade-off between consistency and performance.
While caching can still improve performance by reducing the number of database queries, it may lead to stale data being served from the cache if updates are not properly synchronized.
In such cases, the system must balance the benefits of improved performance against the risk of serving outdated information. Strategies such as implementing cache invalidation mechanisms or setting shorter cache expiration times can help mitigate this trade-off by ensuring that the cached data remains relatively fresh.
It's essential to carefully evaluate the requirements of the application and the frequency of data updates to determine the most suitable caching strategy.
✅ Cache aside strategy
One possible strategy that can be used to introduce a caching mechanism is Cache Aside (or Lazy loading).
Data is cached only when it’s requested.
When an application requests data, it first checks if it is present in the cache, if present, it is returned directly from the cache otherwise it is retrieved from the source (e.g. a database), stored in the cache for future requests and then returned to the application.
in this way we obtain the following advantages:
Reduced latency: frequently requested data is cached, reducing the time required to access it compared to retrieval from slower storage systems such as databases.
Scalability: by reducing the load on back-end systems such as databases, the effective use of caching can help improve the overall scalability of the system.
Improved user experience: By serving frequently accessed data from the cache, users experience faster response times and smoother interactions with applications or websites. This leads to a more satisfying experience.
and disadvantages:
Waste of resources: when storing infrequently used data.
Data consistency: introduces the need to manage synchronization and cache invalidation to ensure that the data are up-to-date and consistent with the source.
👤 User caching in the Magic Link login app
Once the user has logged in, the client calls the server api GET /users/me to retrieve the user's data, here we can introduce a caching mechanism to avoid querying the database.
Let us introduce the mechanism using the following sequence diagram:
Client calls the server api GET /users/me.
Server checks if the user is in the cache.
Not finding the user in cache, the server query the database for the user's information.
User's record is returned.
User is saved in the cache, so we can get it next time without asking the database again.
Cache confirmed.
Server sends the user's information back to the client.
Once data is cached, the server can respond without needing to query the database.
👨💻 Let’s get down to practice
You can download all the code shown directly from my Github repository: https://github.com/marcomoauro/magic-link-auth
We are going to add user caching in the api that is responsible for retrieving the user from the ID.
As a first step, we create the cache.js file, which exposes a class enabling communication with Redis via its client:
import Redis from "ioredis";
const cache = new Redis(process.env.REDIS_URL);
export default class Cache {
static get = async (key) => {
const value = await cache.get(key);
return value ? JSON.parse(value) : null;
}
static set = async (key, value, ttl = null) => {
if (!ttl) {
await cache.set(key, JSON.stringify(value));
return
}
await cache.set(key, JSON.stringify(value), "EX", ttl);
}
static delete = async (key) => {
await cache.del(key);
}
static getBitSize = async (key) => {
const bit_size = await cache.sendCommand(new Redis.Command('MEMORY', ['USAGE', key]));
return bit_size;
}
static getClient = () => {
return cache;
}
}
It uses the ioredis NPM library by reading the service url via the environment variable REDIS_URL.
This class exposes get and set methods with optional specification of TTL (Time to Live) and the client to interact directly with the methods offered by the library, so that I do not have to reimplement all the methods such as getbit and setbit that I used for the Bloom Filter.
You can find the episode here:
I modified the get method of User.js model to read and write the user into the cache:
static get = async ({id, email, username}) => {
log.info('Model::User::get', {id, email, username})
// check user in cache
if (id) {
const user = await Cache.get(`user/${id}`)
if (user) return user
}
const params = []
let query_sql = `
select *
from users
where true
`;
if (id) {
query_sql += ` and id = ?`;
params.push(id);
}
if (email) {
query_sql += ` and email = ?`;
params.push(email);
}
if (username) {
query_sql += ` and username = ?`;
params.push(username);
}
const rows = await query(query_sql, params);
if (rows.length !== 1) throw new APIError404('User not found.')
const user = User.fromDBRow(rows[0])
// set to cache
await Cache.set(`user/${id}`, user, 60 * 5)
return user
}
If the user is searched by id we first check if we have their information saved in cache. If we find it there, we return it without having to look it up in the database. If it's not in the cache, then we search it in the database, save it in the cache for next time, and then return it.
After saving a user in the cache, we set the data expiry time (TTL) to 5 minutes (60s * 5). This ensures that users who haven't used the app for a while are automatically removed from the cache, freeing up memory. When they use the app again, their data will be cached once more.
I also modified the update method in User.js to delete the corresponding data from the cache whenever there's an update:
static update = async (id, params_to_update) => {
log.info('Model::User::update', {id, params_to_update})
if (params_to_update.length === 0) {
throw new APIError422('At least one parameter must be provided to update the user.')
}
const {username} = params_to_update
const params = []
let query_sql = `
update users
set
`;
if (username) {
query_sql += ` username = ?`;
params.push(username);
}
query_sql += ` where id = ?`;
params.push(id);
await query(query_sql, params);
// invalidate cache
await Cache.delete(`user/${id}`)
const user = await User.get({id})
return user
}
This operation is essential because without it, inconsistencies could arise. If we update information in the database without updating the cached record, there could be a mismatch. Therefore, each time we update, we invalidate the cached result to ensure accuracy.
🌟 Top lectures of the week
3 software development principles I wish I knew earlier in my career
Preparing for behavioral interviews
Cross-team drama draining your team?
And that’s it for today! If you are finding this newsletter valuable, consider doing any of these:
🍻 Read with your friends — Implementing lives thanks to word of mouth. Share the article with someone who would like it.
📣 Provide your feedback — We welcome your thoughts! Please share your opinions or suggestions for improving the newsletter, your input helps us adapt the content to your tastes.
💬 Chat with me — If you have any doubts or curiosity, please write to me, I will be happy to answer you!
I wish you a great day! ☀️
Marco
Thanks so much for the mention Marco!
Nice article! Very good use of images and code snippets to showcase the implementation and that cache-aside fits better here than write-through or other mechanisms.