<?php
namespace App\Services;
use App\Models\Order;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class DashboardMetrics
{
private const CACHE_VERSION = 'v3';
private const TTL = 900; // 15 minutes
public static function tag(int $teamId): string
{
return "metrics:{$teamId}";
}
public function revenueSummary(int $teamId, string $from, string $to): array
{
$key = sprintf('%s:revenue:%d:%s:%s', self::CACHE_VERSION, $teamId, $from, $to);
return Cache::tags([self::tag($teamId)])->remember($key, self::TTL, function () use ($teamId, $from, $to) {
return Order::query()
->where('team_id', $teamId)
->whereBetween('created_at', [$from, $to])
->select(DB::raw('DATE(created_at) as day'))
->selectRaw('COUNT(*) as orders')
->selectRaw('SUM(total_cents) as revenue_cents')
->groupBy('day')
->orderBy('day')
->get()
->toArray();
});
}
public static function forget(int $teamId): void
{
Cache::tags([self::tag($teamId)])->flush();
}
}
<?php
namespace App\Observers;
use App\Models\Order;
use App\Services\DashboardMetrics;
class OrderObserver
{
public function saved(Order $order): void
{
DashboardMetrics::forget($order->team_id);
// A moved order invalidates both the old and new team's dashboards.
if ($order->wasChanged('team_id')) {
$original = (int) $order->getOriginal('team_id');
if ($original !== (int) $order->team_id) {
DashboardMetrics::forget($original);
}
}
}
public function deleted(Order $order): void
{
DashboardMetrics::forget($order->team_id);
}
}
<?php
namespace App\Providers;
use App\Models\Order;
use App\Observers\OrderObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(\App\Services\DashboardMetrics::class);
}
public function boot(): void
{
Order::observe(OrderObserver::class);
}
}
<?php
namespace App\Http\Controllers;
use App\Services\DashboardMetrics;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function __construct(private DashboardMetrics $metrics)
{
}
public function revenue(Request $request): JsonResponse
{
$validated = $request->validate([
'from' => ['required', 'date'],
'to' => ['required', 'date', 'after_or_equal:from'],
]);
$teamId = (int) $request->user()->current_team_id;
$summary = $this->metrics->revenueSummary(
$teamId,
$validated['from'],
$validated['to'],
);
return response()->json([
'team_id' => $teamId,
'range' => $validated,
'series' => $summary,
]);
}
}
This snippet shows the read-through cache pattern applied to an expensive analytics query in Laravel, together with the write-side invalidation that keeps it from going stale. The core idea is to compute a costly aggregate once, store it under a stable key, and serve every subsequent request from memory until an underlying row changes.
In DashboardMetrics service, the revenueSummary method wraps the heavy query in Cache::tags(...)->remember(...). The key is namespaced per team and per date range so different tenants and filters never collide, and a version segment (CACHE_VERSION) allows a global flush by bumping a constant during deploys. The closure that produces the value is only executed on a miss; on a hit the grouped selectRaw aggregation over orders is skipped entirely. Tagging the entry with metrics:{teamId} is what makes targeted invalidation possible — a single Cache::tags(...)->flush() clears every range and metric for that team without touching other tenants. Note that tagged caches require a store like Redis or Memcached; the file-based store does not support them.
The trade-off of any cache is staleness, so OrderObserver closes the loop. Laravel fires the observer's saved and deleted hooks on every create, update, and delete, and each one calls DashboardMetrics::forget for the affected team_id. Because invalidation is keyed on a tag rather than a specific range, the code does not need to know which date buckets a given order affects — it drops them all and lets the next read recompute. This favors correctness and simplicity over surgical precision.
In AppServiceProvider, Order::observe(OrderObserver::class) registers the hook so the busting happens automatically wherever orders are written, including inside jobs and console commands. DashboardController simply asks the service for the summary; it neither knows nor cares whether the value came from cache. A subtle pitfall worth noting: observers do not fire for bulk update/delete queries that bypass Eloquent, so those code paths must call forget explicitly. This pattern fits dashboards where reads vastly outnumber writes and a brief recompute on the next request is acceptable.
Related snips
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = false
Bullet.bullet_logger = true
Bullet.console = true
N+1 query detection with Bullet gem
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
Share this code
Here's the card — post it anywhere.