Result
- Invalidation no longer depends on every update function remembering it
- Edits from other services, admin tools or direct SQL also clear the cache
- Wrappers for single objects, lists and collections, schema-checked
Stack
Node.jsTypeScriptRedisPostgreSQL (triggers, pg_notify)zod@panth977/cache
Built at my day job, where I lead the software team behind the Envizom platform. The code belongs to the company; this write-up comes from my public blog post. The reusable parts are open source as @panth977/cache and @panth977/cache-redis.
The problem
The usual cache-aside pattern had the invalidation code living inside updateUser and deleteUser. A new update function could forget it. Related data (an organization, a module's expiry) needed its own invalidation. A schema change meant renaming key prefixes by hand. And an edit from another backend, a worker or a direct database change left a stale value for up to 24 hours.
What I built
- Caching moved into a wrapper around the function, so the read-check-set code exists once.
- All invalidation for an entity moved into one change handler.
- PostgreSQL triggers write every change into a changes table and call
pg_notify; the backend listens, reads the new rows, and runs the handlers. A listened-to table records which listener handled which change, and old changes are deleted after 20 minutes. - Function wrappers with zod input and output schemas, so a cached value with an old shape is not served.
Result
Cache invalidation follows the data, not the code path that changed it. A change made from another service, an admin tool or plain SQL clears the right keys. The blog post has the full SQL and code.