Caching a Product Catalogue With Redis + FastAPI

A product catalogue is the most-read data in a shopping app and one of the least-changed. Every time someone opens the app, they load a category grid. A few times a week, someone in the team changes a price or a photo. That imbalance is what makes caching worth the trouble.
We built the backend for a React Native shopping app for a clothing brand, on FastAPI and PostgreSQL, with Redis caching the catalogue. The app launches in 120ms hot and 900ms cold. The client is under NDA, so I'll keep this general and focus on the pattern and the decisions that come with it.
Why a catalogue suits caching
Three things make it a good candidate:
It's read far more than it's written. Many customers get the same response. Everyone browsing "coats" sees the same list. A slightly stale answer for a short time is usually harmless.
If your data fails any of those, be careful. Carts, stock at checkout and anything specific to one user are poor candidates.
The pattern in plain words
This is cache-aside. When a request comes in for a category:
Look in Redis under a key for that category. If it's there, return it. PostgreSQL never hears about the request. If it isn't, query PostgreSQL, store the result in Redis with an expiry, and return it.
The API becomes cheap for the common case, and the database only does work when something has changed or expired.
Three decisions that matter more than the code
Key design. One key per category is simple and easy to reason about. If the shape of the response ever changes, include a version in the key, so old and new app versions don't read each other's data.
Expiry. A short expiry is a safety net, not a strategy. If it's your only way of getting fresh data, then every price change is visible only after the timer runs out.
Invalidation. This is the one people skip. When someone edits a product from the admin panel, the affected keys need to be cleared or refreshed straight away. A customer seeing an old price is a support ticket. Decide this at the start, because it's hard to bolt on later.
There's also a subtler problem. When a popular key expires, many requests can miss at the same moment and all hit the database together. Staggering expiry times slightly, or letting only one request rebuild the key while the others wait, avoids this.
What I wouldn't cache
Cache the browsing surface, not the transaction. Prices and stock should be checked against the source of truth at checkout, not read from a cache. Being fast on the category grid is nice. Charging the wrong price is not.
Measure both sides
Backend caching fixes the server part of a slow launch. It doesn't fix the rest. Track your cache hit rate and the API's response time, but also measure launch time on the device, and on a budget Android phone, not a flagship. A fast API behind a slow app still feels slow.
When not to bother
If your catalogue is small and traffic is modest, PostgreSQL with a sensible index is probably fine. Add Redis when you can show the database read is the bottleneck, not because it's on every architecture diagram.
Takeaways
Cache what's read often and changed rarely.
Design your invalidation before you write the cache.
Don't cache anything that decides money or stock.
Measure hit rate on the server and launch time on the phone.
How do you handle invalidation in your catalogues: clearing keys on write, short expiry, or something else? I'd like to hear what's worked.

