kai-init wrote a reply+100 XP
2w ago
Very insightful qustions again. There's a few optimisations I've done to keep the package performant for hydration. New Model initlisation for each type of model is only done once, and cloned to setRawAttributes for bypassing laravel magics like boot, observer registration etc. It is still slightly more cpu run time than manually caching it with Cache::remeber() but not by much.
I've done some quick benchmark on CPU run time for direct db access, manual cache::remember and using normcache if you are interested in the detail:

For standalone count and exists they would bypass the package competely for now. Currently the package only handles caching instance where model is returned. There are other model related counts that's handled by the package. The count from a standard paginate is cached via count:{model}:v{version}:{hash} key and withCount, withSum (eloquent aggregates) are cached via agg:{table}:{parentId}:{column}:{function}:{relation}:{constraintHash}:v{relatedVersion}
kai-init wrote a reply+100 XP
2w ago
Hi @imrandevbd That’s a really solid question. It's fully normalized. I made a conscious choice to never embed child models.
The eloquent builder override somewhat handles normlization by default. It intercepts the standard eager-loading flow and routes it through a few different strategies depending on the relation.
Basically anything but a BelongsTo relation gets a seperate query cache for the list of models to load.
- BelongsTo: Since we already has the child IDs, It skip the query cache entirely and just do a batch MGET against the entity store.
- HasMany: It intercept that whereIn query and cache just the child IDs in their own query cache (query:{child}:v{v}:{hash}).
- Many-to-Many: These get a specialized "pivot cache" so It can map the IDs while keeping the unique join-table data (the pivot attributes) normalized (query:{child}:v{v}:{hash})
- Through: Similar logic for Through relations, it bridge the distant models with a "through cache" that's version sensitive to the target and the bridge model.
kai-init started a new conversation+100 XP
2w ago
Hey everyone 👋
Over the past few months, I’ve been working on a small open-source package called laravel-normcache.
It actually started from a problem I kept running into across a few different projects. I found myself repeatedly building the same kind of caching layer to avoid redundant Eloquent data sitting everywhere in Redis, so eventually I thought: why not turn it into a reusable package.
A lot of the ideas are inspired by patterns used in things like Redux/Apollo normalized stores, and I’ve been experimenting and refining things as I use it across real projects.
The main idea behind laravel-normcache is normalizing cached Eloquent results instead of storing entire query collections repeatedly. It stores individual models and query ID maps separately, so when one model changes, every cached query containing that model automatically reflects the update.
Query cache → "posts where active=1, page 2" → [4, 7, 12]
Model cache → post:4 → { id:4, title:..., body:... }
→ post:7 → { id:7, title:..., body:... }
→ post:12 → { id:12, title:..., body:... }
Some things it currently does:
- Automatic caching + invalidation with just a single trait
- Avoids storing duplicate model data across query caches
- Instant consistency across cached query results
- O(1) invalidation using versioned cache keys
- Uses dramatically less Redis memory compared to traditional query caching approaches
There’s still plenty I want to improve and optimize over time, but I felt it was finally in a good enough place to share with the community.
Here’s a quick benchmark comparing direct DB queries vs laravel-model-cache for reference:

I’d genuinely appreciate any feedback, criticism, ideas, or contributions from people more experienced than me. I’m mainly here to learn and improve as an open-source developer.
Repository: https://github.com/kai-init/laravel-normcache
Thanks for reading 🙏
kai-init wrote a reply+100 XP
2w ago
Honestly I think that's what I would do as well. Use the cached data for view and check DB again for any operations.