databases storage and caching

PSR-6 Cache Pools

The main skill is understanding the two PSR-6 concepts: a cache pool manages storage, and a cache item represents one key and value.

Pools And Items

A PSR-6 pool returns an item for a key. The item reports whether the key was found, exposes its value, accepts a new value, and can be given an expiry.

PHP example
<?php

declare(strict_types=1);

function cacheItemSnapshot(string $key, mixed $value, bool $hit): array
{
    return [
        'key' => $key,
        'value' => $value,
        'hit' => $hit,
    ];
}

print_r(cacheItemSnapshot('product.summary.42', ['name' => 'Desk lamp'], true));

// Prints:
// [key] => product.summary.42
// [hit] => 1

In real code, a package provides implementations of Psr\Cache\CacheItemPoolInterface and Psr\Cache\CacheItemInterface. Application code depends on those interfaces rather than a specific Redis or filesystem client.

Read, Rebuild, Save

The normal flow is: ask the pool for an item, return its value on a hit, otherwise rebuild the value and save it.

PHP example
<?php

declare(strict_types=1);

function productSummaryPlan(bool $cacheHit): array
{
    if ($cacheHit) {
        return [
            'source' => 'cache item',
            'action' => 'return the cached value',
        ];
    }

    return [
        'source' => 'repository',
        'action' => 'load, set the item value, set expiry, and save the item',
    ];
}

print_r(productSummaryPlan(false));

// Prints:
// [source] => repository
// [action] => load, set the item value, set expiry, and save the item

A miss must be a normal path. Cache storage can expire, restart, evict values, or become unavailable.

Check isHit() Before Reading

A cached value may legitimately be null. That means the value alone cannot reliably tell you whether the key was found.

PHP example
<?php

declare(strict_types=1);

function explainCacheLookup(bool $isHit, mixed $value): string
{
    if (!$isHit) {
        return 'cache miss';
    }

    return $value === null ? 'cache hit containing null' : 'cache hit containing a value';
}

echo explainCacheLookup(true, null) . PHP_EOL;
echo explainCacheLookup(false, null) . PHP_EOL;

// Prints:
// cache hit containing null
// cache miss

With PSR-6, call isHit() before trusting get().

Expiry

Cache items can expire after a relative TTL or at a specific time.

PHP example
<?php

declare(strict_types=1);

function expiryPlan(int $ttlSeconds): array
{
    return [
        'method' => 'expiresAfter',
        'ttl_seconds' => $ttlSeconds,
    ];
}

print_r(expiryPlan(300));

// Prints:
// [method] => expiresAfter
// [ttl_seconds] => 300

In a real PSR-6 item, expiresAfter(300) means the item should expire in 300 seconds. expiresAt($dateTime) uses an absolute date and time. Choose TTLs based on how stale the data may safely become.

Saving Immediately Or Later

save() writes an item immediately. saveDeferred() lets the pool queue a write for later, and commit() asks the pool to persist queued items.

PHP example
<?php

declare(strict_types=1);

function saveStrategy(int $itemCount): string
{
    if ($itemCount === 1) {
        return 'save immediately';
    }

    return 'save items deferred, then commit once';
}

echo saveStrategy(12) . PHP_EOL;

// Prints:
// save items deferred, then commit once

Deferred saves can reduce backend round trips when storing several items. Do not assume queued writes are durable until they have been committed successfully.

Multiple Keys

Pools can fetch, delete, and check several keys. Batch operations help reduce network calls when the backend is remote.

PHP example
<?php

declare(strict_types=1);

function productSummaryKeys(array $productIds): array
{
    return array_map(
        static fn (int $id): string => 'product.summary.' . $id,
        $productIds
    );
}

echo implode(', ', productSummaryKeys([42, 51, 88])) . PHP_EOL;

// Prints:
// product.summary.42, product.summary.51, product.summary.88

The pool interface includes operations such as getItems(), hasItem(), deleteItem(), deleteItems(), and clear().

Key Rules

PSR-6 keys are strings. Implementations must support keys using letters, numbers, underscore, and dot, and reserve some characters for future use: {}, (), /, \, @, and :.

PHP example
<?php

declare(strict_types=1);

function productCacheKey(int $productId): string
{
    return 'product.summary.' . $productId;
}

echo productCacheKey(42) . PHP_EOL;

// Prints:
// product.summary.42

Use central key builders. They keep reads, writes, deletes, and tests aligned.

Deleting And Clearing

Delete the specific item when source data changes. clear() removes everything in the pool and should be used deliberately.

PHP example
<?php

declare(strict_types=1);

function invalidationMethod(string $changeType): string
{
    return match ($changeType) {
        'single_product_updated' => 'deleteItem for the product key',
        'cache_format_changed' => 'clear the dedicated pool during deployment',
        default => 'identify affected items and delete them',
    };
}

echo invalidationMethod('single_product_updated') . PHP_EOL;

// Prints:
// deleteItem for the product key

A dedicated pool for one responsibility can make broad clears less risky than sharing one pool for unrelated data.

Exceptions And Backend Failure

PSR-6 cache exceptions implement Psr\Cache\CacheException. Invalid keys use Psr\Cache\InvalidArgumentException. The implementation decides which storage failures become exceptions.

Caching should normally improve performance rather than determine correctness. Application code should know whether it can fall back to the repository when caching fails.

PHP example
<?php

declare(strict_types=1);

function cacheFailurePlan(bool $sourceCanBeReloaded): string
{
    return $sourceCanBeReloaded
        ? 'log the cache failure and load from the source'
        : 'surface the failure because no safe fallback exists';
}

echo cacheFailurePlan(true) . PHP_EOL;

// Prints:
// log the cache failure and load from the source

What To Check

Before moving on, make sure you can:

  • explain the difference between a cache pool and a cache item;
  • check isHit() before reading an item value;
  • rebuild and save values after a miss;
  • set relative or absolute expiry;
  • explain save() versus saveDeferred() and commit();
  • use predictable PSR-6-safe keys;
  • delete targeted items after source data changes;
  • treat a backend failure according to the application's fallback rules.

Practice

Practice: Model A PSR-6 Product Cache

Build a small in-memory PHP model of the PSR-6 read, miss, expiry, and save flow for product summaries.

Requirements

  • Create an item with a key, value, hit status, and TTL.
  • Check the hit status separately from the cached value.
  • Rebuild and save the item on a miss.
  • Use a PSR-6-safe key such as product.summary.42.
  • Demonstrate a miss followed by a hit.
  • Include a deferred-save plan for several items.

You do not need to install a PSR-6 package. Model the behaviour so the important API concepts are visible.

Show solution

This small model shows why PSR-6 uses item objects and a separate hit check.

PHP example
<?php

declare(strict_types=1);

final class CacheItem
{
    public function __construct(
        public readonly string $key,
        public mixed $value = null,
        public bool $hit = false,
        public ?int $ttlSeconds = null,
    ) {
    }
}

final class CachePool
{
    /** @var array<string, CacheItem> */
    private array $items = [];

    public function getItem(string $key): CacheItem
    {
        return $this->items[$key] ?? new CacheItem($key);
    }

    public function save(CacheItem $item): void
    {
        $item->hit = true;
        $this->items[$item->key] = $item;
    }
}

function productSummary(CachePool $pool, int $productId): array
{
    $key = 'product.summary.' . $productId;
    $item = $pool->getItem($key);

    if ($item->hit) {
        return $item->value;
    }

    $item->value = [
        'id' => $productId,
        'name' => 'Desk lamp',
    ];
    $item->ttlSeconds = 300;
    $pool->save($item);

    return $item->value;
}

function deferredSavePlan(int $itemCount): string
{
    return $itemCount > 1
        ? 'saveDeferred for each item, then commit'
        : 'save immediately';
}

$pool = new CachePool();
$before = $pool->getItem('product.summary.42');

productSummary($pool, 42);

$after = $pool->getItem('product.summary.42');

echo $before->hit ? 'hit' : 'miss';
echo PHP_EOL;
echo $after->hit ? 'hit' : 'miss';
echo PHP_EOL;
echo $after->value['name'] . PHP_EOL;
echo $after->ttlSeconds . PHP_EOL;
echo deferredSavePlan(3) . PHP_EOL;

// Prints:
// miss
// hit
// Desk lamp
// 300
// saveDeferred for each item, then commit

In production, use a package that implements the real PSR-6 interfaces. This model exists to make the item lifecycle clear.