mikebronner/laravel-model-caching
Automatically caches Eloquent model and relationship queries, then invalidates them when data changes—no manual cache keys. Add a trait to your models to cut database load on read-heavy pages with self-invalidating, Laravel-native caching.
What it is. A drop-in Eloquent query-result cache: add the Cachable trait (or extend CachedModel) to a model, and get/first/find/all/paginate/pluck/value/exists, aggregations, and eager-loaded with() relationships are cached and auto-invalidated from Eloquent model events (created/updated/deleted). MIT, ~2.4k stars, Laravel 11–13, PHP 8.2+, 335+ integration tests, current release cadence. The value proposition is real but narrow: it removes hand-written cache keys and hand-written invalidation, which is the single most common source of stale-data incidents in Laravel codebases.
Architecture fit. This is an interception layer, not a library you call. It hooks the Eloquent model/query-builder lifecycle, so it sits on your most load-bearing abstraction. That is what makes it cheap to adopt and expensive to reason about: any behavior it doesn't intercept bypasses both the cache and the invalidation. The package is explicit about the boundaries — lazy-loaded relationships, queries with custom select() clauses, and inRandomOrder() are not cached; queries inside transactions are not flushed on commit (manual flushCache() required). Invalidation is tag-granular per model, not row- or query-granular: any write to a model flushes that model's entire cached set. On write-heavy entities (sessions-like tables, counters, activity feeds, users) the cache degenerates into constant cold reads plus invalidation churn, so the "100–900% fewer queries" figure only holds for read-skewed, comparatively static data (CMS pages, catalogs, taxonomy, config-like tables).
Integration feasibility. Low friction to prove, moderate friction to productionize. Install is one composer require plus a trait on a base model; there is no schema change, no migration, no sidecar. The real integration work is environmental and procedural:
MODEL_CACHE_FALLBACK_TO_DB=false (the default), a Redis outage becomes an application outage on every cached read path. MODEL_CACHE_FALLBACK_TO_DB=true is essentially mandatory for anything customer-facing; treat it as a required config decision, not a default.DB::table() updates, bulk UPDATE statements, queue jobs using raw SQL, a second application or reporting tool writing the same database, DBA hotfixes — leaves stale rows in cache with no expiry guarantee documented in the README. This is the single largest risk and it is organizational, not technical.Technical risk. Ranked:
withCacheCooldownSeconds() deliberately suppresses invalidation for a window — correct for "recent comments" widgets, unacceptable for balances, inventory, permissions, or anything financial).cache-prefix or the per-model $cachePrefix. use-database-keying separates connections, not tenants sharing one database. A misconfigured or dynamic prefix is a cross-tenant data exposure bug, not a performance bug.forever() entries. Plan maxmemory policy, memory budgets, and key-count alerting from day one.Key questions for engineering before approval.
use-database-keying plus a per-tenant prefix fully isolate tenants in our topology?User model excluded from the pilot (README explicitly warns against cool-down on it)?Stack fit. Clean fit for a conventional Laravel 11–13 monolith or modular monolith on PHP 8.2+ with Redis already in the stack. Poor fit if Redis is absent, if the app is read-light or uniformly write-heavy, if significant data mutation happens outside Eloquent, or if the data is multi-tenant with a cache-prefix story that hasn't been designed yet.
Migration path (additive, reversible, no schema change).
App\Models\BaseModel with the Cachable trait and migrate 2–3 deliberately chosen models: high read volume, low write volume, non-financial, not User. Keep the default Model for everything else so the blast radius is bounded and rollback is deleting a trait.INFO keyspace. Establish baseline numbers so the 100–900% claim is verified on your queries, not taken from the README.DB::table() writes, bulk updates, and any second writer to the same database; convert or explicitly exclude those models. Add a lint/review checklist item rather than relying on memory.Compatibility and configuration.
model-cache store in config/cache.php and point MODEL_CACHE_STORE at it, so invalidation churn and memory pressure don't evict sessions, queues, or rate-limit keys out of the shared store.MODEL_CACHE_FALLBACK_TO_DB=true in every environment above local.MODEL_CACHE_ENABLED=false in test and local by default, with a small number of tests that explicitly exercise caching against a real Redis.$cachePrefix (or a globally derived prefix) designed and reviewed before enabling on any tenant-scoped model.Sequencing across the org.
Maintenance. Low code-level maintenance: no schema, one dependency, MIT, thick test suite, active releases including a Laravel 13 line. The maintenance burden shifts from "write cache keys" to "police the write path and the upgrade." Budget for: reading release notes for cache key/tag format changes (which the maintainers themselves classify as breaking-ish), validating each Laravel major bump against builder interception, and an owned decision log of which models are cached and why. A README-plus-releases maturity rating means you are relying on the test suite and the diff, not on extensive docs — assign one engineer as the designated owner who has read the internals.
Support. The hard support cases are not "the cache is broken" but "the data is wrong and the cache is why." Without deliberate preparation, on-call will not have the vocabulary. Required runbook entries: how to verify a stale-vs-fresh discrepancy, how to flush per-model vs globally, how to disable caching in an incident (MODEL_CACHE_ENABLED=false deploy, or ModelCache::runDisabled()), how to detect a Redis outage vs an invalidation storm, and who owns cache-prefix correctness for tenants. Add a first-response step to every data-correctness escalation: "was this row written outside Eloquent?"
Scaling. Read scaling is the point and it works well for read-skewed data — Redis is the right substrate. Write scaling is where it degrades: per-model tag invalidation means a hot model's cache is under near-constant churn, so the effective cache lifetime approaches zero and you pay invalidation overhead for nothing. Mitigations exist but are commitments, not switches: cool-down windows (buying throughput with deliberate staleness), sharding hot models across cache prefixes, or simply not caching hot-write models. Redis capacity planning needs to assume entries without a documented short TTL — set maxmemory, choose eviction policy consciously (evicting model-cache keys is survivable; evicting sessions is not, which is why a dedicated store matters), and alert on memory slope, not just absolute usage. Expect the post-deploy cold-start spike and, if key formats change, plan a warm-up or staggered rollout.
Failure modes to design for.
Ramp-up. Mechanically trivial (one trait), conceptually non-trivial. Plan roughly half a day of engineering onboarding covering: what is and isn't cached (the explicit non-cached list is the curriculum), why with() is mandatory and lazy loading silently defeats the cache, why select() bypasses it, why DB::table() writes are now a data-integrity concern, and how to disable caching per query and for seeders. Add three lines to PR review: did you write outside Eloquent, did you add a cached model without a measured win, did you change a tenant prefix. Total realistic adoption cost for a mid-size team: 2–4 weeks elapsed including infrastructure, guardrails, and a measured pilot — with a clear revert path (remove the trait) if the pilot doesn't beat baseline query counts and latency on representative traffic.
How can I help you explore Laravel packages today?