thekaziomar / laravel-atomic-cache
A simple, stampede-proof, and tag-aware caching package for Laravel.
Package info
github.com/kaziomarsh24/laravel-atomic-cache
pkg:composer/thekaziomar/laravel-atomic-cache
Requires
- php: ^8.2 || ^8.3 || ^8.4 || ^8.5 || ^8.6
- illuminate/cache: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/database: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- phpunit/phpunit: ^10.0 || ^11.0
Suggests
- ext-zlib: Required for payload gzip compression on large cache items.
Provides
None
Conflicts
None
Replaces
None
README
A high-performance, stampede-proof caching package for Laravel. Designed for modern web applications handling concurrent bursts, large payloads, multi-table model invalidations, and non-Redis environments.
use KaziOmar\AtomicCache\Facades\AtomicCache; // Standard caching with auto-compression (>50KB) & self-healing $users = AtomicCache::remember('users.active', 3600, fn() => User::where('active', 1)->get()); // Cache stampede protection (atomic lock so 1000 concurrent requests only run 1 query) $report = AtomicCache::rememberWithLock('monthly_report', fn() => Report::generate());
๐ Table of Contents
- Why Laravel Atomic Cache?
- Requirements
- Installation
- Core Features & Usage
- 1. Basic Caching
- 2. Stampede Protection (rememberWithLock)
- 3. Auto Payload Compression (Gzip)
- 4. Virtual Tagging Engine (File & Database Fallback)
- 5. Cache TTL Jitter (Thundering Herd Prevention)
- 6. User-Scoped Caching (rememberForUser)
- 7. Fluent Chainable Builder
- 8. Blade Fragment Caching
- 9. Artisan CLI Command
- Eloquent Model Lifecycle (AutoClearsCache)
- Service & Repository Layer (HasAtomicCache)
- Configuration Reference
- Testing
- Credits & License
๐ก Why Laravel Atomic Cache?
| Feature | Standard Laravel Cache | Laravel Atomic Cache |
|---|---|---|
| Stampede Protection | โ All concurrent requests hit DB | โ Atomic Lock (1 worker computes, others wait & read) |
| Large Payload Size | โ Raw uncompressed serialization | โ Auto Gzip (>50KB cuts Redis RAM by ~70%) |
| Driver Tag Support | โ Crashes on File/Database stores | โ Virtual Tagging (works across all stores without Redis) |
| Corrupted Data | โ Throws crash exception to user | โ Self-Healing (cleans key, regenerates silently) |
| Simultaneous Expiry | โ All keys expire in same second | โ TTL Jitter (random variance prevents herd spikes) |
| Model Invalidation | โ ๏ธ Fragile observers before commit | โ DB::afterCommit (prevents transaction race conditions) |
| Pagination Caching | โ ๏ธ Page 1 and 2 collide unless manual | โ Auto Query Hash (page/filter automatically isolated) |
โ๏ธ Requirements
- PHP:
8.2,8.3,8.4,8.5, or8.6 - Laravel Framework:
10.x,11.x,12.x, or13.x - PHP Extensions:
ext-zlib(optional for compression, built-in on 99% of PHP installs)
๐ฆ Installation
Install via Composer:
composer require thekaziomar/laravel-atomic-cache
Publish the optional configuration file:
php artisan vendor:publish --tag="atomic-cache-config"
๐ Core Features & Usage
1. Basic Caching
Works seamlessly like native Laravel cache, supporting flexible parameter ordering:
use KaziOmar\AtomicCache\Facades\AtomicCache; // Standard signature: remember($key, $ttl, $callback) $posts = AtomicCache::remember('posts.all', 3600, function () { return Post::where('published', true)->get(); }); // Alternative signature: remember($key, $callback, $ttl) $posts = AtomicCache::remember('posts.all', fn() => Post::all(), 3600); // Global helper shortcut $posts = atomic_cache('posts.all', fn() => Post::all(), 3600);
Understanding the Parameters:
| Parameter | Type | Description |
|---|---|---|
$key |
string |
Unique Cache Identifier in your cache store (e.g. 'posts.all', 'home.banners'). Using {resource}.{action} or {module}:{query} is the industry standard convention to prevent key collisions and enable easy searching in Redis (redis-cli KEYS "atomic:posts.*"). |
$ttl |
int |
Time-To-Live in seconds (e.g. 3600 = 1 hour, 86400 = 1 day). Specifies how long the value remains cached. If omitted, uses config('atomic-cache.default_ttl', 3600). |
$callback |
Closure |
Data Producer Callback. Executes only when the cache is empty or expired. The returned value is automatically compressed (if >50KB), stored in cache, and returned. |
// Standard CRUD methods: AtomicCache::put('key', $value, 3600); // Store an item directly $val = AtomicCache::get('key', default: null); // Retrieve an item $exists = AtomicCache::has('key'); // Check existence $val = AtomicCache::pull('key'); // Retrieve and immediately delete AtomicCache::clear('key'); // Delete item (alias of forget)
2. Stampede Protection (rememberWithLock)
During sudden traffic surges (e.g. after a cache flush or key expiry), hundreds of requests hit your database simultaneously trying to recalculate the same data.
rememberWithLock acquires an atomic distributed lock. Only one process executes the query, while concurrent requests wait up to 5 seconds and read the freshly cached result:
$analytics = AtomicCache::rememberWithLock('dashboard.stats', function () { return HeavyAnalyticsService::calculate(); }, ttl: 3600, lockSeconds: 10, waitSeconds: 5);
3. Auto Payload Compression (Gzip)
Large Eloquent collections or API responses can consume gigabytes of Redis/Memcached RAM.
Whenever a cached payload exceeds 50 KB (configurable), Atomic Cache automatically compresses it with gzcompress:
- Reduces Redis memory usage by ~70% to 80%.
- Decompression is handled transparently on retrieval.
- If payload is smaller than 50 KB, it stores without compression overhead.
4. Virtual Tagging Engine (File & Database Fallback)
In Laravel, calling Cache::tags(['users']) on file or database cache drivers crashes with BadMethodCallException: This cache store does not support tagging.
Atomic Cache introduces a Virtual Tag Versioning Engine:
- On Redis / Memcached: Uses native high-performance driver tags.
- On File / Database / SQLite / Array: Tracks atomic version counters per tag (e.g.
_tag_v:users = 1). - When
clearTags(['users'])is called, it increments the tag version to2. - Subsequent reads automatically experience a cache miss! Tag invalidation works in local development without Redis!
// Clear a single model item: flushes tag "users:42" AtomicCache::clearItem('users', 42); // Clear an entire collection: flushes tag "users" AtomicCache::clearCollection('users'); // Clear custom tags AtomicCache::clearTags(['dashboard', 'reports']);
5. Cache TTL Jitter (Thundering Herd Prevention)
If you cache 1,000 keys for 3,600 seconds, they will all expire at the exact same microsecond, causing a sudden CPU and database spike.
withJitter adds a small random variance (e.g. ยฑ5%) to the TTL:
// TTL of 3600s will randomly vary between 3420s and 3780s AtomicCache::withJitter()->remember('feed.popular', fn() => getFeed(), 3600); // Or pass an explicit variance (e.g. 10%) AtomicCache::withJitter(0.10)->remember('feed.popular', fn() => getFeed(), 3600);
6. User-Scoped Caching (rememberForUser)
Avoid manually concatenating Auth::id() into your cache keys:
// Automatically scopes key to "atomic:user:{auth_id}:dashboard_nav" $menu = AtomicCache::rememberForUser('dashboard_nav', function () { return Auth::user()->navigationMenu(); }); // Or scope to a specific user ID $profile = AtomicCache::rememberForUser('profile', fn() => User::find(99), userId: 99);
7. Fluent Chainable Builder
You can fluently combine any optimizations in a clean builder chain:
$data = AtomicCache::store('redis') ->tags(['finance', 'reports']) ->ttl(7200) ->withLock(lockSeconds: 15, waitSeconds: 5) ->withJitter(0.05) ->remember('annual_revenue', function () { return RevenueCalculator::compute(); });
8. Blade Fragment Caching
Cache expensive HTML blocks, navigation bars, or sidebar widgets directly in Blade templates:
@atomicCache('sidebar_categories', 3600) <div class="sidebar"> @foreach(\App\Models\Category::with('subcategories')->get() as $category) <h3>{{ $category->name }}</h3> @foreach($category->subcategories as $sub) <p>{{ $sub->title }}</p> @endforeach @endforeach </div> @endAtomicCache
9. Artisan CLI Command
Manage and clear cache tags or keys from terminal, cron jobs, or CI/CD deployment scripts:
# Clear a specific cache tag or key php artisan atomic-cache:clear users # Clear multiple tags php artisan atomic-cache:clear dashboard # Clear all atomic cache entries php artisan atomic-cache:clear --all
๐งฉ Eloquent Model Lifecycle (AutoClearsCache)
Add the AutoClearsCache trait to any Eloquent model. Whenever a record is created, updated, or deleted, its cache tags are automatically flushed only after the active database transaction successfully commits (DB::afterCommit):
namespace App\Models; use Illuminate\Database\Eloquent\Model; use KaziOmar\AtomicCache\Traits\AutoClearsCache; class User extends Model { use AutoClearsCache; // Optional: automatically invalidate related cache tags when a user changes protected array $relatedCacheTags = ['dashboard', 'admin_stats']; }
When $user->save() or $user->delete() executes:
- Waits for
DB::commit()(if transaction rolls back, cache is untouched). - Automatically invalidates single item tag:
users:{id}. - Automatically invalidates collection tag:
users. - Automatically invalidates related tags:
dashboard,admin_stats.
๐๏ธ Service & Repository Layer (HasAtomicCache)
Standardize your caching architecture across services with the HasAtomicCache trait:
namespace App\Services; use App\Models\User; use KaziOmar\AtomicCache\Traits\HasAtomicCache; class UserService { use HasAtomicCache; // Optional service-level configuration properties: protected bool $cachingEnabled = true; // Set to false to bypass cache (useful in tests/debugging) protected int $cacheTTL = 7200; // Custom default TTL in seconds for this service protected bool $cachePerUser = false; // Scope all operations to the authenticated user ID protected string $cacheResource = 'users'; // Base resource tag (defaults to table/plural class name) public function find(int $id) { return $this->cacheItem($id, fn() => User::findOrFail($id)); } public function getActiveUsers() { return $this->cacheCollection('active', fn() => User::where('active', 1)->get()); } public function update(int $id, array $data) { $user = User::findOrFail($id); $user->update($data); // Clear both single item and list caches $this->clearItemCache($id); $this->clearCollectionCache(); return $user; } }
Service Configuration Properties
You can customize caching per service by declaring any of these optional properties:
| Property | Type | Default | Purpose |
|---|---|---|---|
$cachingEnabled |
bool |
true |
Service Cache Toggle. Set to false during automated testing or debugging to bypass caching completely and always fetch fresh database records. |
$cacheTTL |
int |
3600 |
Service-Level Expiry. Defines the default cache lifetime in seconds for this specific service without overriding global config. |
$cachePerUser |
bool |
false |
Automatic User Isolation. When set to true, all cache keys generated in this service are automatically prefixed with the logged-in user's ID (auth()->id()), preventing user data leaks. |
$cacheResource |
string |
(auto) | Resource Tag & Namespace. Specifies the cache tag and key prefix (e.g. 'users', 'orders'). Defaults automatically to the model's table or pluralized class name (UserService -> 'users'). |
Safe Argument Normalization
If a service method receives complex arguments (e.g. Closure, Eloquent Model, or Request), HasAtomicCache safely converts them to unique hashes/identifiers before serialization. You will never encounter Exception: Serialization of 'Closure' is not allowed.
Automatic Pagination & Filter Isolation
When calling cache() or cacheCollection(), the trait automatically inspects request()->query().
- Request
GET /users?page=1andGET /users?page=2are automatically stored under separate, isolated cache keys. - Search filters like
?filter=active&sort=descautomatically receive independent cache entries.
โ๏ธ Configuration Reference
Publish config/atomic-cache.php to customize package behavior:
return [ // Master switch to enable/disable caching (useful in local tests) 'enabled' => env('ATOMIC_CACHE_ENABLED', true), // Default TTL in seconds (1 hour) 'default_ttl' => env('ATOMIC_CACHE_DEFAULT_TTL', 3600), // Specific cache store (null = default Laravel cache store) 'store' => env('ATOMIC_CACHE_STORE', null), // Global key namespace prefix 'prefix' => env('ATOMIC_CACHE_PREFIX', 'atomic'), // Stampede distributed locking settings 'stampede' => [ 'enabled' => env('ATOMIC_CACHE_STAMPEDE_ENABLED', true), 'lock_seconds' => env('ATOMIC_CACHE_LOCK_SECONDS', 10), 'wait_seconds' => env('ATOMIC_CACHE_WAIT_SECONDS', 5), ], // Automatic Gzip payload compression 'compression' => [ 'enabled' => env('ATOMIC_CACHE_COMPRESSION_ENABLED', true), 'threshold' => env('ATOMIC_CACHE_COMPRESSION_THRESHOLD', 51200), // 50 KB 'level' => env('ATOMIC_CACHE_COMPRESSION_LEVEL', 6), ], // Self-healing from corrupted cache payloads 'self_healing' => [ 'enabled' => env('ATOMIC_CACHE_SELF_HEALING', true), 'log_corrupted' => env('ATOMIC_CACHE_LOG_CORRUPTED', true), ], // Random variance on expiry time 'jitter' => [ 'enabled' => env('ATOMIC_CACHE_JITTER_ENABLED', false), 'percentage' => env('ATOMIC_CACHE_JITTER_PERCENTAGE', 5), // ยฑ5% ], ];
Environment Variables (.env)
| Key | Default | Description |
|---|---|---|
ATOMIC_CACHE_ENABLED |
true |
Enable or disable atomic caching globally |
ATOMIC_CACHE_DEFAULT_TTL |
3600 |
Default Time-To-Live in seconds |
ATOMIC_CACHE_STORE |
null |
Cache driver store to use (redis, file, etc.) |
ATOMIC_CACHE_PREFIX |
atomic |
Key namespace prefix |
ATOMIC_CACHE_STAMPEDE_ENABLED |
true |
Enable atomic locking by default |
ATOMIC_CACHE_LOCK_SECONDS |
10 |
Maximum lock duration during computation |
ATOMIC_CACHE_WAIT_SECONDS |
5 |
Maximum wait time for concurrent requests |
ATOMIC_CACHE_COMPRESSION_ENABLED |
true |
Auto-compress payloads exceeding threshold |
ATOMIC_CACHE_COMPRESSION_THRESHOLD |
51200 |
Compression threshold in bytes (50 KB) |
ATOMIC_CACHE_JITTER_ENABLED |
false |
Enable TTL variance globally |
ATOMIC_CACHE_JITTER_PERCENTAGE |
5 |
Jitter variance percentage (ยฑ5%) |
๐งช Testing
Run the test suite using PHPUnit:
composer test
๐จโ๐ป Credits
- Kazi Omar Faruk (@kaziomarsh24)
๐ License
The MIT License (MIT). Please see License File for more information.