The write-through pattern

The write-through pattern

A comprehensive overview of the write-through pattern to improve application performance with Redis.

Welcome back, this is part 4 of the "caching patterns with Redis" series. Click here to get to the third part.

A write-through cache reverses the order of how the cache is populated. Instead of lazy-loading the data in the cache after a cache miss like in the cache-aside pattern, the cache is proactively updated immediately following the primary database update. To see how this pattern functions, consider the image below.

  1. The application, batch, or backend process updates the primary database.

  2. Immediately afterwards, the data is also updated in the cache.

  3. Return a response to the mutator.

This approach has the following advantages:

  1. The cache is always in sync with the database thus no stale data will be sent to the client.

A disadvantage to the write-through pattern is that the request needs to wait for the cache to be updated thus increasing the response time.

Let's see how we can use the write-through pattern in our posts application. Since we are using JWT tokens for our auth, there is a need to add expiration logic since the tokens don't expire. For our case, we can have a JWT versioning. When the user logs out, we'll increment the JWT version by one.

export const logout = async ({ email }: IUser) => {
  // Update the JWT version in the db
  const user = await db.users.update({
    where: { email },
    data: { jwtVersion: { increment: 1 } },
    select: { email: true, name: true, id: true, jwtVersion: true },
  });
  // Update the user in Redis
  await redis.set("user-" + email, JSON.stringify(user));
  // Return success
  return true;
};

// Our router will look like this
router.post("/logout", async (req, res, next) => {
  try {
    const data = await logout(req.user);
    res.json(data);
  } catch (error) {
    next(error);
  }
});

// Updated authMiddleware to check for expiration
export const authMiddleware = async (
  req: Request,
  _res: Response,
  next: NextFunction
) => {
  try {
    // Auth token in passed in aid header
    const aid = req.headers["aid"] as string;
    if (!aid) {
      throw new Error("Auth token has not been provided");
    }
    // Verify the JWT
    const payload: IUser = verify(aid, process.env.JWT_SECRET as string, {
      issuer: "Caching-Code-Login",
      audience: "Caching-Code-Auth",
    }) as IUser;

    // Get the user using the email
    const user = await getUserByEmail(payload.email);

    // Verify the token version
    if (payload.jwtVersion !== user?.jwtVersion) {
      throw new Error("Expired auth token provided");
    }

    if (!user) {
      throw new Error("Authentication failed");
    }

    // Attach a user to the request
    req.user = user;

    //  Successfully authenticated. Call the next handler.
    next();
  } catch (error) {
    next(error);
  }
};

Thank you for reading this far, I hope you are liking it. Feel free to comment on your thoughts and jump into the next blog in the series.