foxws/laravel-modelcache
Cache Eloquent model queries and results with a simple, fluent API for Laravel. Reduce database load by storing frequently accessed model data in your cache store, with automatic invalidation on updates. Lightweight, easy to configure, and works with any Laravel cache driver.
Architecture fit. The package is a thin convention layer over Laravel's cache contracts — a trait (InteractsWithModelCache), a ModelCache facade, a CacheProfile policy object, and a config file. It adds no persistence engine, no queue worker, no schema, and no runtime service. That is a favorable shape for a TPM: the surface we are adopting is small, the blast radius is bounded by the call sites we write, and the exit cost is a use statement per model plus the keys we chose to create.
Fit is good when the product need is small per-model scalars that are nice to keep but not worth a row: playback/resume position, in-progress wizard step, last-viewed tab, an A/B seed, a memoized stat block on a detail page. It converts "we need a table, an index, and a backfill" conversations into one-day stories. Fit is poor when our caching conventions already live in a repository/service layer — the package attaches caching to the model, which creates a second idiom in code review and a second place to look when a value is stale.
Integration feasibility. Low effort, high judgment. Install is one composer require, optional config publish, three env vars (MODEL_CACHE_ENABLED, MODEL_CACHE_STORE, MODEL_CACHE_LIFETIME), and a trait on target models. No migrations, no manual provider registration, no external dependency. The entire engineering cost concentrates in one decision: classifying each value as user-scoped or shared. Keys are suffixed with the authenticated user ID by default, so a value written during a user's request is invisible to guests, other users, console commands, and queued jobs. Overriding cacheNameSuffix() to '' is a small code change that flips a performance feature into a cross-user exposure risk when applied to genuinely per-user data. That classification must be made explicitly before the first PR, not discovered in review.
Technical risk. The dominant risk is the maturity signal, not the code: 2 GitHub stars, 0 dependents, single-maintainer provenance, no downstream validation. That is a bus-factor and long-term-support risk, and it should drive our posture (vendor, pin, budget for a fork) more than any architectural concern. Secondary risks worth naming to stakeholders:
MODEL_CACHE_ENABLED=false disables everything with no error. Every read path needs a working default (modelCached($key, $default)), or the kill switch becomes an outage switch.array or file stores, values vanish on deploy or flush and the feature looks broken in staging. Any value treated as authoritative needs a persistent store — and usually the cache may not be the system of record at all.playback_position, stale values can live up to the one-week default TTL. Version the key name from day one.Key questions.
CacheProfile extension point, or is the trait default sufficient for the first release?hasher/serializer compatible with our existing key-prefix and tenancy conventions?Stack fit. Works on any Laravel app with a configured persistent cache store and a meaningful authenticated-user context on the cached models. It composes with multi-tenant models because the suffix is per-model overridable, so a tenant-scoped suffix is a small extension rather than a fork — but note that it customizes the one thing the package is opinionated about, which means we own that behavior permanently. It does not fit where there is no stable auth context (the default suffix degenerates toward the shared namespace) or where caching is already centralized in a service layer.
Migration path. Purely additive and reversible. Add the trait, start writing new keys, and no existing read path changes until a call site explicitly reads through modelCached. No data migration. Rollback is removing call sites and the trait; cached entries become orphaned keys that expire on their TTL. The only migration-shaped task is a key classification inventory: for each value, record model, key name, owner (user vs. shared), TTL, store, and system-of-record yes/no. That should be a deliverable owned by a named person, not a wiki page — it is the artifact that lets a future owner reason about this without re-reading the source.
Compatibility. Verify the Laravel constraint in composer.json explicitly rather than inferring it from a release date we already can't trust. Set MODEL_CACHE_STORE explicitly instead of letting it fall back to CACHE_STORE; otherwise this package's keys compete with session, fragment, and rate-limit keys for the same memory and eviction budget. Confirm the hasher/serializer defaults against our existing key-prefix and tenancy conventions before writing keys, since changing them afterward invalidates everything already stored.
Sequencing.
MODEL_CACHE_ENABLED as the single ops kill switch and MODEL_CACHE_STORE pointed at Redis (or database), explicitly not inherited.CacheProfile contract — TTL, shouldUseCache(), shouldCacheValue() — so squads don't invent key conventions independently.Maintenance. Assume we may be maintaining a fork. The code surface is small enough to read in half a day and reimplement in a few days, which is the main reason this is a defensible adoption despite the maturity signal. Pin the version; don't float. Budget ongoing ownership of the CacheProfile and the key registry as real recurring cost, because unowned keys become ghost data that nobody can safely delete.
Support. There is effectively no upstream support guarantee from a 2-star, single-maintainer package. Internally, the support story is: one named owner for the profile contract, one named owner for the key registry, and a documented exit path (vendor or reimplement) written down before the pilot ships. Escalation for "the cached value looks wrong" must land with the key owner, not with whoever wrote the page.
Scaling. Cost scales as (models × keys × active users) in cache memory, so a per-user default multiplies cardinality quickly — plan Redis capacity and eviction policy before broad rollout, and confirm what happens to our feature when eviction kicks in. modelCacheRemember() on a cold cache will hit the database concurrently for every request until the first value lands; if the wrapped computation is expensive, that's a stampede risk worth a short lock or a warm-up path. The one-week default TTL means both good news (self-healing staleness) and bad news (a semantics change lingers for a week unless key names are versioned). There is no built-in observability — no hit rates, key inventories, per-key ownership, or cost attribution — so any telemetry we want, we build.
Failure modes to design against.
cacheNameSuffix() to '' on per-user data is a data-exposure incident, not a performance regression. This is the one misconfiguration that requires a review checklist, not just a code convention.Ramp-up. Small. One engineer, roughly half a day to read the source and the docs, then a PR-sized pilot. The real ramp-up cost is organizational: the per-user-by-default namespace has to be taught once, clearly, or it will be discovered the hard way when a queued job or a guest reads a different namespace than the request that wrote it. Budget for one internal write-up plus one review checklist item on cacheNameSuffix() overrides. The framing for stakeholders should stay narrow and honest: "one less table for state we don't need to keep forever," not "a caching strategy" — and if a feature's correctness depends on the cached value being present, it doesn't belong here.
How can I help you explore Laravel packages today?