michael-rubel/lada-cache
Lada Cache is a Redis-based automatic query cache for Laravel. It caches Eloquent queries and invalidates entries when related models change, requiring little configuration while speeding up reads and keeping cached data consistent across scalable applications.
Minimal setup
composer require michael-rubel/lada-cache — pin at least ^0.0.2 if your queries use withCount (see Gotchas).LadaCacheServiceProvider in config/app.php.php artisan vendor:publish --tag=lada-cache-config (confirm the tag in the provider — this package lineage has used more than one).config/database.php → redis (own logical DB index, own key prefix). Do not share it with sessions, queues, or Horizon.lada-cache driver wrapping your real connection), or toggle the enable flag in config/lada-cache.php. It hooks the connection, not your models, so no use Trait step is needed on the Eloquent side.Where to look first
config/lada-cache.php — the whole surface area of the package lives here: Redis connection/prefix, enable flag, logger, and query-tagging behaviour.First use case
A read-heavy endpoint (public dashboard, product listing, API GET that repeats the same 5–10 Eloquent queries). Enable the layer for that connection, hit the endpoint twice, and watch Redis key count plus DB::listen output — second hit should produce zero queries. That's the five-minute proof that justifies or kills the experiment.
Zero-touch retrofit. The main selling point: because it sits at the connection driver, existing controllers/repositories gain caching without edits. This is genuinely useful on a legacy codebase where you can't refactor every query. Enable it on one connection, measure, expand.
Scope it, don't globalise it. Turn caching on per connection (and ideally per app area — read replica, reporting DB, etc.) rather than for the whole application. Write-heavy tables pay an invalidation tax on every INSERT/UPDATE/DELETE; read-only or rarely-changing tables get nearly free wins.
Trust automatic invalidation for model-driven writes. Normal save(), create(), delete() through Eloquent will tag and bust the right rows. This is the happy path and works well for CRUD.
Aggregate loaders are now a supported path — on 0.0.2+. The 0.0.2 release specifically fixed withCount handling, so list endpoints that eager-load counts (Post::withCount('comments')->get()) can be cached without hand-holding. If you have any withCount / relationship-count loading, treat 0.0.2 as your floor and verify with the debugging recipe below before rolling out. Other aggregate shapes (withSum, withAvg, withExists, raw selectRaw aggregates) are not covered by that fix — test each one you actually use.
Pair with HTTP-level caching for hot endpoints. Lada covers the DB layer; a response cache or Cache::remember around the controller return covers the rest. Don't ask the DB layer to solve a whole-response problem.
Test with it off. Add the disable env var to phpunit.xml (<env name="LADA_CACHE_DISABLE" value="true"/>, confirm the exact key in config) so tests don't depend on a Redis instance. Assert query counts with DB::listen in a separate opt-in suite if you want to prove caching works.
Observe before you ship. Turn on the package's logger in staging, sample Redis INFO memory and key counts over a week, and confirm hit rate. If you can't articulate the hit rate, you're carrying invalidation overhead for nothing.
The maintenance question is still the headline — but marginally less bleak. 0.0.1 → 0.0.2 is one community PR (withCount), not a signal of active stewardship: still a 0.0.x re-publish of spiritix/lada-cache, still near-zero adoption, and the better-maintained descendant remains genealabs/laravel-model-caching. Read the 0.0.2 diff before depending on it; it tells you the query-builder hook can be patched, which is encouraging for the architecture but doesn't answer "who fixes the next bug." If 0.0.2 fails to install on your Laravel/PHP version, stop here and use the maintained fork or plain tagged caching.
withCount was broken before 0.0.2. If you inherited an install on 0.0.1 and saw wrong counts, cache misses on every count query, or invalidation that never cleared a count column, that's the bug. Upgrade and re-verify: cache the count query, write a related row, and confirm the response updates. Note the fix only covers withCount — other aggregate selectors need your own test.
Redis is mandatory. This is not "works with any cache store." No Redis, no package. It also means your cache is in-process-adjacent infrastructure that can disappear — design for a cold cache, not a warm one.
Invalidation granularity is table/row based, and joins can over-invalidate. Complex joins, subqueries, and aggregates may end up flushing far more than the row you changed. On write-heavy tables this produces cache thrash that is worse than no cache. Profile write volume before enabling on those tables.
Raw SQL bypasses everything. DB::statement, DB::unprepared, mass insert(), and update() query-builder calls that skip model events will leave stale rows in cache. Any bulk job, importer, or queue worker needs an explicit invalidation call afterwards — this is the #1 source of "works in dev, stale in prod."
Transactions and long-running workers. Invalidation timing relative to commit is the thing to verify on your version. A read inside a long transaction, or a queue worker holding a stale cache across jobs, can serve old data. Keep transactions short and don't cache across tenant boundaries.
Multi-tenancy: isolate your prefixes. Row tags are keyed by ID, so if two tenants share a Redis prefix and DB, you can leak data across them. Give each tenant (or at least each environment) its own logical DB + prefix.
Debugging recipe. Enable the package logger → inspect Redis keys to see which tags a cached key carries → reproduce the write → confirm the tag set was emptied. If invalidation didn't fire, the write bypassed Eloquent; if it fired too broadly, the query joined too much. For aggregate queries, add a step: dump the generated SQL for the withCount/aggregate query and confirm it's being tagged at all rather than silently skipped.
Low lock-in is a real plus. Because it's a connection-driver swap, removal is a config change plus a Redis flush — no code to unwind. Treat that as your escape hatch and set a review date (say, one quarter) to decide whether the hit rate justifies keeping a near-unmaintained 0.0.x dependency.
How can I help you explore Laravel packages today?