
Caching in Laravel is simple. Managing cache keys in a growing, multilingual application… is not.
Most Laravel projects start small. But once you introduce:
- Multiple locales (EN / AR)
- Admin vs Public contexts
- Single record caching
- Dynamic keys (IDs, roles, tenants)
- Multiple developers
Cache keys can quickly become inconsistent and fragile.
In this article, we’ll use a real-life example — caching countries (list + single record) — and answer the real question:
Is using PHP Enums for cache keys best practice — or overengineering?
How Laravel Caching Works
Laravel provides a clean caching API:
Cache::remember()
Cache::get()
Cache::put()
Cache::forget()
Example:
Cache::remember('countries_en', 3600, function () {
return Country::all();
});
This works perfectly. Until your application grows.
The Hidden Problem With String Cache Keys
Let’s say your system supports Arabic and English.
Soon, you might have keys like:
countries_en
countries_ar
countries_plain_names_en
countries_plain_names_ar
country_en_5
country_ar_5
countries_en_admin
countries_ar_admin
Now imagine:
- 50+ keys
- 5 developers
- Frequent refactoring
- Redis debugging
- Cache invalidation after updates
Problems start appearing:
- ❌ Typos (
countires_en) - ❌ Inconsistent ordering (
country_5_envscountry_en_5) - ❌ Duplicate keys
- ❌ Hard-to-find invalidation logic
- ❌ Naming chaos
This is where structure becomes important.
The Structured Solution: CacheKeysRegistry Enum
Instead of scattering strings everywhere, centralize them.
enum CacheKeysRegistry: string
{
case COUNTRIES = 'countries_';
case COUNTRY = 'country_';
case COUNTRIES_PLAIN_NAMES = 'countries_plain_names_';
public function resolve(string|int ...$suffixes): string
{
if (empty($suffixes)) {
return $this->value;
}
return $this->value . implode('_', $suffixes);
}
public function resolveWithLocale(string|int ...$suffixes): string
{
return $this->resolve(app()->getLocale(), ...$suffixes);
}
public function keyWithId(string|int $id): string
{
return $this->value . $id;
}
}
Now cache keys become predictable, structured, and centralized.
Real-Life Example 1: Caching All Countries Per Locale
Instead of:
Cache::remember('countries_' . app()->getLocale(), 3600, function () {
return Country::all();
});
Use:
Cache::remember(
CacheKeysRegistry::COUNTRIES->resolveWithLocale(),
3600,
fn () => Country::all()
);
If locale is en, the generated key:
countries_en
If locale is ar:
countries_ar
Real-Life Example 2: Caching a Single Country by ID
Single record caching is where chaos usually starts.
Instead of:
Cache::remember('country_' . $id . '_' . app()->getLocale(), 3600, function () {
return Country::find($id);
});
Use:
Cache::remember(
CacheKeysRegistry::COUNTRY
->resolveWithLocale($countryId),
3600,
fn () => Country::find($countryId)
);
If:
- ID = 5
- Locale = en
Generated key:
country_en_5
Clear. Standardized. Predictable.
Real-Life Example 3: Admin Countries
If admin users see additional data:
Cache::remember(
CacheKeysRegistry::COUNTRIES
->resolveWithLocale('admin'),
3600,
fn () => Country::forAdmin()->get()
);
Generated key:
countries_en_admin
Enforcing a Naming Convention
We now follow a consistent structure:
resource_locale_identifier
Examples:
countries_en
country_en_5
countries_ar_admin
countries_plain_names_en
This consistency:
- Makes Redis debugging easier
- Prevents key collisions
- Makes bulk invalidation predictable
- Improves system clarity
Why resolveWithLocale() Is Important
Instead of repeatedly writing:
->resolve(app()->getLocale(), $id);
You simply write:
->resolveWithLocale($id);
Benefits:
- ✔ Less repetition
- ✔ No forgotten locale
- ✔ Cleaner code
- ✔ Standardized format
- ✔ Perfect for multilingual apps
Clearing Cache Safely
When country ID 5 updates:
Cache::forget(
CacheKeysRegistry::COUNTRY
->resolveWithLocale(5)
);
When all countries update:
Cache::forget(
CacheKeysRegistry::COUNTRIES
->resolveWithLocale()
);
No magic strings. No guessing.
So… Best Practice or Overengineering?
It IS Best Practice When:
- Your app is multilingual
- You cache both lists and single records
- You have dynamic keys (IDs, roles, tenants)
- Multiple developers work on the project
- Cache invalidation matters
- The project is long-term
Structure prevents chaos.
It CAN Be Overengineering When:
- You have 5 cache keys total
- No localization
- No dynamic suffixes
- Small solo CRUD project
- Short-term prototype
In small systems, plain strings are fine.
Abstraction should solve real problems — not create them.
Architecture Insight
This pattern improves:
- Maintainability
- Refactor safety
- Naming consistency
- IDE support
- Team collaboration
- Debugging clarity
But it adds:
- Slight abstraction
- Minor framework coupling (
app()->getLocale()inside enum)
The key question:
Are you preventing future technical debt — or adding unnecessary complexity?
In growing applications, this pattern prevents chaos before it starts.
Final Verdict
Using a CacheKeysRegistry enum with:
resolve()resolveWithLocale()keyWithId()
is not overengineering in serious Laravel systems.
It is controlled scalability.
If your application:
- Caches country lists
- Caches single country records
- Supports multiple locales
- Has admin/public separation
Then structured cache keys are no longer optional.
They are architecture.
But if your app is tiny?
Keep it simple.
Architecture should grow with your system — not ahead of it.
