Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
LaravelPHPJune 24, 20264 min read

Laravel Eloquent Cache-Aside: Implementing Decorators for Consistency

Master Laravel Eloquent Cache-Aside patterns using the Decorator pattern. Ensure data consistency and slash latency with this clean, production-ready guide.

LaravelPHPEloquentCachingArchitectureDesign PatternsBackend

Last month, we faced a classic "stale data" nightmare on our primary user-dashboard service. We had cache-aside logic scattered across five different controllers, and inevitably, someone forgot to clear a key after an update() call, leaving users staring at outdated information for roughly 15 minutes.

If you’re tired of manually managing Cache::forget() calls inside your business logic, it’s time to move toward a more architectural solution. By using the Decorator Pattern to wrap your data access, you can enforce Laravel Eloquent Cache-Aside consistency without polluting your service layer.

The Problem with Inline Caching

We first tried the standard approach: adding Cache::remember() blocks directly inside our repository methods. While it worked for a week, it quickly became a maintenance burden. We needed to keep the database and Redis in sync, but the update() methods were completely decoupled from the get() methods.

We were constantly duplicating logic. If you're building a system where data integrity is non-negotiable, you should consider integrating the Laravel Repository Pattern: Decoupling Data Access from Business Logic to isolate these concerns before applying decorators.

Implementing the Decorator Pattern for Eloquent

Instead of bloating your models or repositories, define an interface for your data access layer. Then, create a "Cached" implementation that wraps the "Eloquent" implementation.

Here is a simplified look at how we structure this:

PHP
interface UserRepositoryInterface {
    public function find(int $id): User;
}

class EloquentUserRepository implements UserRepositoryInterface {
    public function find(int $id): User {
        return User::findOrFail($id);
    }
}

class CachedUserRepository implements UserRepositoryInterface {
    public function __construct(
        protected UserRepositoryInterface $repository,
        protected \Illuminate\Contracts\Cache\Repository $cache
    ) {}

    public function find(int $id): User {
        return $this->cache->remember("user:{$id}", 3600, function () use ($id) {
            return $this->repository->find($id);
        });
    }
}

By binding UserRepositoryInterface to CachedUserRepository in your AppServiceProvider, your controllers remain completely unaware that caching is even happening. They just call $repository->find($id) and enjoy the speed.

Ensuring Data Consistency with Transactions

The real challenge with Data Consistency isn't reading; it's writing. If your cache invalidation fails, you have a problem. When dealing with complex state changes, we often lean on the Transactional Outbox Pattern in Laravel: Ensuring Data Consistency to ensure our side effects—like clearing cache keys—are as atomic as possible.

To keep things deterministic, wrap your invalidation logic inside a database transaction callback. This ensures that the cache is only purged if the database operation actually succeeds:

PHP
public function update(int $id, array $data): User {
    return DB::transaction(function () use ($id, $data) {
        $user = $this->repository->update($id, $data);
        $this->cache->forget("user:{$id}");
        return $user;
    });
}

If the database write fails, the transaction rolls back, and the cache remains untouched. It’s not perfect—network partitions between Redis and your DB can still occur—but it’s significantly more robust than firing forget() calls at the end of a controller method.

Why This Architecture Wins

  1. Separation of Concerns: Your business logic doesn't care about TTLs or cache tags.
  2. Testability: You can unit test your CachedUserRepository by mocking the underlying EloquentUserRepository and asserting that the cache is called.
  3. Transparency: You can toggle caching off globally by simply swapping the binding in your Service Provider.

We’ve found that for high-traffic endpoints, combining this with Laravel Cache Warming: Predictive Pipelines with Redis Streams provides the best performance. Instead of waiting for a cache miss, the stream pushes the updated record into the cache immediately after the transaction commits.

Trade-offs and Caveats

You’ll notice that this approach introduces a slight overhead in service resolution. We’re essentially adding a layer of indirection. In most Laravel apps, this cost is negligible—measured in microseconds—compared to the latency of a database query. However, if you are working on a system with extreme constraints, you might find that the complexity of maintaining these decorators outweighs the benefits.

I’m still experimenting with how to handle cache tags more effectively within this pattern. Currently, if we need to purge a user's entire related cache (posts, comments, etc.), the decorator starts to get messy with tag arrays. For now, we keep it simple, but I’m looking into implementing a more robust "Invalidator" service that the decorators can inject.

If you’re starting this implementation, start with your most expensive queries. Don't try to wrap everything at once. Build the decorator, bind it, and watch your database CPU usage drop. It’s a clean way to handle Eloquent caching that keeps your codebase readable and your data consistent.

FAQ

Q: Does this work with Laravel's built-in model caching? A: It works alongside it, but this decorator approach is more explicit. It avoids the "magic" that often makes built-in model caching difficult to debug in production.

Q: How do I handle cache tags with the decorator? A: You can pass an array of tags to the decorator constructor or define an invalidate() method on your interface that accepts tags, allowing the decorator to handle the Cache::tags([...])->forget() call internally.

Q: Is the Decorator pattern overkill for small apps? A: Probably. If your app is a simple CRUD dashboard, stick to Cache::remember(). Use this pattern when you have multiple developers working on the same entities and you need to enforce a consistent caching policy across the entire system.

Back to Blog

Similar Posts

LaravelPHPJune 24, 20264 min read

Laravel Repository Pattern: Decoupling Data Access from Business Logic

Master the Laravel repository pattern to decouple your data access from business logic. Learn how to keep your code clean, testable, and maintainable.

Read more
LaravelPHP
June 24, 2026
4 min read

Laravel Proxy Pattern for Eloquent Lazy Loading Optimization

Laravel Proxy Pattern implementations can solve memory bloat in large Eloquent relationships. Learn to build deterministic lazy loading for high-scale apps.

Read more
LaravelPHPJune 24, 20264 min read

Laravel Eloquent Hydration Optimization: Reducing Reflection Overhead

Laravel Eloquent hydration often creates hidden bottlenecks under heavy load. Learn to implement custom hydrators to bypass reflection and scale your app.

Read more