Stop Manually Clearing Laravel Cache Let Observers Do It For You
If you’ve been building APIs with Laravel for a while, you’ve probably written code like this:
public function index()
{
return ProductResource::collection(
cache()->remember('products', 60 * 60 * 24, function () {
return Product::all();
})
);
}It works great until it doesn’t.
The bug that almost always shows up
Here’s a pattern I’ve seen (and honestly written myself) more times than I’d like to admit:
You cache something like products because it rarely changes. Then, a few weeks later, someone adds a "create product" feature. You remember to clear the cache there:
public function store(Request $request)
{
$product = Product::create($request->validated());
cache()->forget('products');
return new ProductResource($product);
}Cool, that works. Then a month later, someone adds an “update product” endpoint. Or a bulk import script. Or a seeder that runs in production for a one off fix. Or an admin panel action that updates the product name directly through a different service class.
Every single one of those places needs to remember to call cache()->forget('products'). Miss just one, and now you have a production bug where users swear they added new data but it "isn't showing up." You spend twenty minutes debugging queries and controllers before you realize it's not a query problem, it's a stale cache problem.
The real issue isn’t that cache()->forget() is wrong. It's that cache invalidation scattered across controllers and services doesn't scale. Every new place that touches the model becomes a new place you can forget to add cache clearing to.
The fix is to stop treating cache clearing as “something I do in the controller” and instead tie it directly to the model itself.
Step 1: Cache forever instead of for 24 hours
If you’re clearing the cache automatically whenever the data changes, there’s no reason to set an expiry time at all. Instead of guessing “24 hours feels about right,” just cache it forever, and let invalidation happen only when the data actually changes.
Swap remember() for rememberForever():
// app/Http/Controllers/Api/ProductController.php
class ProductController extends Controller
{
public function index()
{
return ProductResource::collection(
cache()->rememberForever('products', function () {
return Product::all();
})
);
}
}No TTL, no arbitrary numbers, no “is 24 hours too long or too short” debates. The cache lives until something explicitly clears it.
Step 2: Let an Observer watch the model
Laravel’s Eloquent Observers let you hook into model lifecycle events -created, updated, deleted, restored, and more in one dedicated class, instead of sprinkling that logic across every controller method that touches the model.
Get Nadun Sameera’s stories in your inbox
Join Medium for free to get updates from this writer.
Generate one:
php artisan make:observer ProductObserver --model=ProductAttach it to the model using the #[ObservedBy] attribute (available since Laravel 10+):
// app/Models/Product.php
use App\Observers\ProductObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy([ProductObserver::class])]
class Product extends Model
{
//
}Now, inside the observer, clear the cache whenever the model changes:
// app/Observers/ProductObserver.php
use App\Models\Product;
class ProductObserver
{
public function created(Product $product): void
{
cache()->forget('products');
}
}That’s it. Create a product from the API, from a seeder, from Tinker, from an admin panel, from a queued job it doesn’t matter. As long as it goes through Eloquent, the observer fires and the cache gets cleared. The next request to /products rebuilds the cache from a fresh database query, and every visit after that is served from cache again.
Step 3: Don’t stop at created() cover update and delete too
The one gap in a lot of tutorials (and in a lot of real codebases) is only handling created(). But products don't just get created they get renamed, deactivated, or deleted. If you only clear the cache on creation, you'll still hit the exact bug this article started with, just on updates instead.
Cover all the lifecycle events that matter:
// app/Observers/ProductObserver.php
use App\Models\Product;
class ProductObserver
{
public function created(Product $product): void
{
cache()->forget('products');
}
public function updated(Product $product): void
{
cache()->forget('products');
}
public function deleted(Product $product): void
{
cache()->forget('products');
}
}Since all three do the same thing, you can simplify it with a single method and Laravel’s saved/deleted events, or just extract a private helper:
class ProductObserver
{
public function created(Product $product): void
{
$this->clearCache();
}
public function updated(Product $product): void
{
$this->clearCache();
}
public function deleted(Product $product): void
{
$this->clearCache();
}
private function clearCache(): void
{
cache()->forget('products');
}
}Now, however the model changes, the cache is guaranteed to reflect it on the next read. No controller, service, or script has to remember anything.
A note on cache keys as your app grows
Hardcoding a string like 'products' works fine for a single, app wide cache entry. But once you start caching per user or per relationship data (e.g. products-for-user-{$id}), a plain forget() call won't be enough you'll be clearing one key at a time instead of a whole group. That's when it's worth looking into cache tags (if your cache driver supports them, like Redis or Memcached) so an observer can flush an entire group of related keys at once instead of tracking individual key names by hand.
One small Laravel 13 note
If you’re upgrading an older app: Laravel 13 changed the framework’s auto-generated cache key prefixes to use hyphens instead of underscores. It won’t affect a hardcoded key like 'products' from this article, but it's worth knowing if your app leans on Laravel's default, framework-generated cache prefixes anywhere.
Wrapping up
The pattern is simple, but it fixes a genuinely common source of bugs:
- Use
rememberForever()instead of guessing a TTL. - Let an Observer clear the cache on
created,updated, anddeleted. - Attach it with
#[ObservedBy]so it's automatic no controller code required.
The result: cache invalidation lives in exactly one place, tied to the model itself, instead of being duplicated (and inevitably forgotten) across every controller, service, and script that touches your data.









