SIGNALS Documentation
API Reference

Caching Strategy

Cache-aside architecture, CacheService tags-or-fallback helper, cache map, TTLs, invalidation, and query-budget sentinels.

Binding

No generic model-cache / cache-through-Eloquent layer. Invalidation risk in a financial system is unacceptable; N+1s are fixed at the query level. Strengthen explicit cache-aside instead.

Drivers

Driver Tags Multi-get Notes
redis (production primary) Yes MGET / pipeline db0=cache, db1=queues, db2=sessions, db3=reverb
database (self-host fallback / some test envs) No WHERE IN Cache reads count as SQL queries in perf audits
array (tests) Yes (Laravel 13) In-memory Process-local only
file No Sequential Dev only

All tags-dependent call sites go through App\Services\CacheService, which degrades to untagged keys when Cache::supportsTags() is false and logs a once-per-process warning. Tag flushes on untagged warehouses use an explicit key list or callback; otherwise entries expire on TTL.

Request-scoped memoisation

Prefer $this->app->scoped() bindings for services that memoize in-memory within a request (Octane-safe — reset each request/job):

  • CustomFieldDefinitionResolver, WidgetDataCache, RateResolver
  • AvailabilityService, ShortageDetector, status registries, and similar

Use once() for pure in-request closures. Use Cache::memo() only when the same durable cache key is read multiple times in one request and you still want the store hit on the first call.

Do not use singletons for request-memoising services under Octane.

Cache map

Domain Keys / tags TTL Invalidation Stats domain
Settings settings:all / tag settings Indefinite Write via SettingsService::flush settings
Navigation navigation:{userId} / tag navigation 1h (default; tunable) Registration, role, module changes navigation
Dashboard widgets dashboard:widget:… + generation key 5 min (default; tunable) Generation bump (WidgetDataCache::flush) widgets
Custom field schema schema:document.* / tag schema-registry 1h Schema / CF definition writes custom-fields
Status / API registries *:statuses:all, api:endpoints, … / tags list-entries, … Indefinite or 1h List-value / catalogue writes registries
Account insights accounts:insights:{id} / tag account-insights 5 min (tunable) Account financial mutations insights
Line-item picker catalogue catalogue-items:catalogue:{warehouseId|none} / tag product-catalogue 5 min CatalogueItem, accessory, or product-rate write (CatalogueItemSearchService::flushCatalogue) registries
i18n strings i18n:strings:{locale} Indefinite Language import / create i18n
Exchange rates exchange-rates:{base} 24h (tunable) Rate write (via CurrencyService)
Permissions Spatie built-in Spatie Role/permission writes —
Plugin registry plugin cache keys Indefinite Plugin lifecycle —

Hit / miss instrumentation

CacheService maintains cheap forever counters:

  • cache-stats:{domain}:hits
  • cache-stats:{domain}:misses

Domains: settings, navigation, widgets, custom-fields, registries, insights, i18n. Exposed on the Admin → System → Caching panel.

Dev vs prod query counts

Perf-harness numbers (signals:perf-audit) on CACHE_STORE=database count every cache get/put as a SQL query. Redis-warm baselines are materially lower for the same pages. Query budget sentinels encode both:

Sentinel Database driver Redis driver Duplicates
dashboard ≤40 queries ≤25 queries ≤19
Index pages (accounts, rentals, catalogue_items, invoices) ≤60 ≤60 ≤19

Enforced by App\Services\PerfAudit\QueryBudgetGuard and the perf Pest group (tests/Feature/PerfAudit/QueryBudgetSentinelTest.php) for the deferred gate.

What we deliberately do not cache

  • Eloquent models via a generic observer / trait layer
  • Per-row financial snapshots beyond the named domain caches above
  • Anything whose invalidation trigger cannot be named in this map