Its an Application level caching, where the application checks whether the data is present in the cache (In our case redis). If data is present it returns the data. Else checks from the database and stores it in Cache and then returns the data to client.

Steps:
- The Application (Fast API), checks whether the requested data is present in the cache (redis).
- If the data present in the cache (redis) it gets the data and returns the value.
- If the data is not present in the cache (cache miss), it will check the details from the primary database (mongodb).
- If the data is available it returns the data to the application.
- The application now sets the data to Cache for future hits (cache hit).
Cache Hit: If the requested data is present in the cache.
Cache Miss: If the requested data is not present in the cache.
Implementation
We are going to create a mock todo application. Let’s spin redis and mongo db using dockers,
For redis,
docker run -p 6379:6379 --rm redislabs/redismod:latest

For Mongo,
docker run --name mongo_container --rm mongo

To load the data into mongo, either you can use the below script, or restore from the dump file.
from pymongo.mongo_client import MongoClient
from faker import Faker
fake = Faker()
# Mongo Db Connection
MONGO_CONN_STR = "mongodb://localhost:27017"
mongo_client = MongoClient(MONGO_CONN_STR)
database = mongo_client['test']
collection = database["todos"]
for itr in range(1, 1000000):
data = {
"id": str(itr),
"name": fake.name(),
"description": fake.text(100)
}
collection.insert_one(data)
print(data)
or
docker exec -i mongo_container sh -c 'mongorestore --archive' < 2l_data.dump
Lets create the fastapi app.py,
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from pymongo.mongo_client import MongoClient
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
app = FastAPI()
# Mongo Db Connection
MONGO_CONN_STR = "mongodb://localhost:27017"
mongo_client = MongoClient(MONGO_CONN_STR)
database = mongo_client['test']
collection = database["todos"]
class Todo(BaseModel):
id: str
name: str
description: str = None
@app.post("/todos/")
def create_todo(todo: Todo):
collection.insert_one(todo.dict())
return {}
@app.get("/todos/{todo_id}", response_model=Todo)
def read_item(todo_id: str):
from_cache = r.hgetall(todo_id)
if from_cache:
return from_cache
todo = collection.find_one({"id": todo_id}, {'_id':0})
r.hmset(todo_id, todo)
if todo:
return todo
raise HTTPException(status_code=404, detail="Item not found")
Here i am exposing 2 apis,
/todos post – To create a record to db.
/todos/{todo_id} get – To get the record from resource.
Run the app
uvicorn app:app --port 8000
For the testing pupose, we are using python locust module to send requests,
locust_file.py
mport random
import logging
from locust import task, constant
from locust.contrib.fasthttp import FastHttpUser
log = logging.getLogger("rest-api-short-urls")
todo_ids = range(1, 200000)
class LocustClient(FastHttpUser):
wait_time = constant(0)
host = "http://localhost:8000/todos"
def __init__(self, environment):
""" Class constructor."""
super().__init__(environment)
@task
def test_get_todos(self):
try:
url = random.choice(todo_ids)
with self.client.get(f'/{url}', name='get todo id') as resp_of_api:
if resp_of_api.status_code == 200:
log.info("API call resulted in success.")
else:
log.error("API call resulted in failed.")
except Exception as e:
log.error(f"Exception occurred! details are {e}")
Run the locust-file,
locust -f locust_file.py -t 1m -u 100 -r 100
Drawbacks
- For every request there is a cache miss. If the requested items aren’t repeating in a good percentage, then this will create latency.
- What happens if the record been updated in the database, but not in redis. Do we need to write on every update ?
- In a distributed system, if a node fails, it will be replaced by a new empty node. This will increase the latency. (this can be overcomed with replication of the data).
- If your application is a write-heavy workload, then this will add a good latency.
When to use ?
- If your application is read heavy, then you can go with this approach.
- If your application is not having any dynamic change of data, then this approach will be a good one.