cache-coherence
v1.0.1
Published
Structural cache invalidation for Mongoose: write hooks that keep a two-tier cache honest, with a companion change-stream watcher for writes the hooks never see.
Readme
cache-coherence
Structural cache invalidation for MongoDB/Mongoose: a write hook that keeps a two-tier cache honest, without relying on every call site remembering to invalidate.
The problem
async function updateProduct(id: string, changes: Partial<Product>) {
await ProductModel.updateOne({ _id: id }, changes);
await cache.invalidate(`product:${id}`); // <- easy to forget, easy to miss
}Relying on every write call site to remember an invalidation line is a discipline problem, not a guarantee. Miss one — a bulk script, a different service writing to the same database, a filter-based updateMany your invalidation code doesn't know how to key — and reads go silently stale.
This library makes invalidation a property of the schema instead:
attachCacheCoherence(ProductSchema, { cache, announceOnInvalidate: true });Every write through ProductModel now evicts its own cache entry automatically, no matter which call site triggered it.
For writes that bypass your Mongoose model entirely (another service, a raw driver script, a mongosh session), pair this with cache-coherence-watcher — a standalone process that reads MongoDB's change stream directly and closes that gap structurally.
Install
npm install cache-coherence ioredisioredis and mongoose are peer dependencies — bring your own versions (mongoose >= 6, ioredis >= 5). prom-client is an optional peer, only needed if you use the cache-coherence/metrics entry point.
Quickstart
import { RedisBroadcaster, RedisRemoteStore, TwoTierCache, attachCacheCoherence } from "cache-coherence";
import { Redis } from "ioredis";
import mongoose, { Schema } from "mongoose";
// Redis pub/sub needs a connection dedicated to subscribing - it can't
// also issue ordinary commands - so the remote store and broadcaster each
// need their own client.
const remoteClient = new Redis(process.env.REDIS_URL);
const subscriberClient = new Redis(process.env.REDIS_URL);
const cache = new TwoTierCache({
namespace: "my-app",
remote: new RedisRemoteStore(remoteClient),
broadcaster: new RedisBroadcaster({ subscriber: subscriberClient, publisher: remoteClient }),
defaultTtlSeconds: 300,
});
const ProductSchema = new Schema({ name: String, price: Number, stock: Number }, { collection: "products" });
attachCacheCoherence(ProductSchema, { cache, announceOnInvalidate: true });
export const ProductModel = mongoose.model("Product", ProductSchema);
// reads
const cached = await cache.lookup({ scope: "products", id: productId });
// or, read-through in one call:
const product = await cache.loadOrCompute(
{ scope: "products", id: productId },
() => ProductModel.findById(productId).lean(),
);How it fits together
TwoTierCache— a hot in-process LRU tier backed by a shared Redis tier.evict()is the single choke point every invalidation path (this hook, the watcher) goes through.attachCacheCoherence(schema, options)— wires a Mongoose schema sosave,updateOne,findOneAndUpdate,deleteOne, and friends auto-evict. Idempotent — safe to call more than once on the same schema.RedisRemoteStore/RedisBroadcaster— the reference Redis backends. Both are built against small interfaces (RemoteStore,Broadcaster), so a non-Redis backend is a ~20-line implementation, not a fork.InMemoryRemoteStore/InMemoryBroadcaster— dependency-free stand-ins for single-instance use or fast tests.cache-coherence/metrics— a separate entry point exportingcreatePromMetricsRecorder(registry). Kept out of the main entry point so importing this library never requiresprom-clientto be installed unless you actually use it.
Known limitation
The in-process hook can't resolve a document id for a filter-based updateMany/deleteMany (Mongoose's middleware doesn't expose which documents matched), and it can only ever see writes made through this app's own Mongoose model. Both gaps are exactly what cache-coherence-watcher closes, by reading MongoDB's change stream directly instead of trusting any particular code path.
Full docs
Architecture, benchmarks, a live before/after demo, and the full Known Limitations list: github.com/Prajin0802/cache-coherence.
MIT licensed.
