skip to content

In PSR-6, what do CacheItemPoolInterface::saveDeferred() and commit() do, and what may a pool do with deferred items before commit()?

level: middleimportance: should knowfreq 25%

answer

  1. queue now, persist later
  2. commit() persists everything outstanding
  3. pool may persist early
  4. getItem() sees the deferred value
  5. both return bool, not exceptions

basics

~20 s

saveDeferred() queues an item for later persistence and commit() persists every queued item, so a pool can batch writes. A pool may persist deferred items earlier, must not lose them, and must return them from getItem() before commit.

solid answer

~40 s

In PSR-6, `save($item)` persists an item immediately, while `saveDeferred($item)` only queues it, letting the pool use bulk-set operations its storage supports. `commit()` persists every outstanding deferred item and returns `true` if all were saved or there were none. The specification leaves timing to the pool: it may persist deferred items before `commit()`, for example on `save()`, on a size limit or in a destructor, but it must eventually persist them and not lose data, and a `getItem()` for a deferred key must return the deferred value. `saveDeferred()` returns `false` if the item could not be queued, or if a commit was attempted and failed. Failures are reported through these booleans, since implementations should trap storage errors rather than let them bubble, so a caller that needs certainty must check `commit()`'s return value.

code

php · 18 lines
php
<?php
declare(strict_types=1);

use Psr\Cache\CacheItemPoolInterface;

/** @param array<string, float> $rates */
function warmRates(CacheItemPoolInterface $pool, array $rates): bool
{
    foreach ($rates as $pair => $rate) {
        $item = $pool->getItem('rate.' . $pair)->set($rate)->expiresAfter(600);
        $pool->saveDeferred($item);
    }

    // Same pool, before commit: the deferred value must already be visible.
    // $pool->getItem('rate.EURUSD')->get() returns the queued rate.

    return $pool->commit(); // false if any deferred item failed to persist
}

go deeper

for a junior

Recall that save() writes now, saveDeferred() queues, and commit() writes everything queued.

for a middle

Explain the five deferred-item rules, especially early persistence and read-your-own-deferred-writes, and why failures come back as booleans.

for a senior

Use deferred saves for bulk warm-ups, always finish with a checked commit(), and never build correctness on deferral being atomic.

for a principal

Judge when batching cache writes is worth the added failure modes, compared with PSR-16 setMultiple() or no batching at all.

## Immediate versus deferred saves In **PSR-6**, a `CacheItemPoolInterface` offers two ways to persist an item you obtained with `getItem()` and changed with `set()`: | Method | Effect | Returns | |---|---|---| | `save($item)` | persists the item immediately | `true` on success, `false` on error | | `saveDeferred($item)` | queues the item to be persisted later | `false` if it could not be queued, or if a commit was attempted and failed; `true` otherwise | | `commit()` | persists every outstanding deferred item | `true` if all were saved or there were none; `false` otherwise | The point of deferral is **batching**. Many storage engines can write many keys in one round trip; a pool that collects deferred items can send them together instead of one network call per item. ## What the specification promises about deferred items The **Deferred** definition in PSR-6 sets these rules: 1. A pool **may** delay persisting a deferred item to take advantage of bulk operations. 2. It **must** ensure deferred items are eventually persisted and data is not lost. 3. It **may** persist them before the caller asks, using any logic it likes: an object destructor, persisting everything on the next `save()`, a timeout, or a maximum-items check. 4. When the caller invokes `commit()`, all outstanding deferred items **must** be persisted. 5. A request for a deferred item **must** return the deferred, not-yet-persisted item. Rule 5 means code in the same process reads its own writes: after `saveDeferred()`, `getItem()` for that key sees the new value even if storage has not been touched yet. ## What callers should and should not assume - Do **not** assume nothing is written until `commit()`: rule 3 allows early persistence, so deferral is not a transaction and there is no rollback. - Do **not** rely on a destructor to flush: some pools do, but the specification only lists it as one permitted strategy. Call `commit()` explicitly when the batch is done. - **Do** check the return value of `commit()` when correctness depends on the writes; errors surface as `false`, not exceptions. - Other processes cannot see deferred items until they are persisted; in practice only the pool object holding the queue can answer with them. ## Why failures are booleans PSR-6's error-handling section says caching should never be a critical part of application functionality. Implementations **must not** throw exceptions other than those the interfaces define, and **should** trap errors from the underlying store rather than let them bubble, logging or reporting them instead. So a storage outage typically appears as `save()`, `saveDeferred()` or `commit()` returning `false`, and a later `getItem()` returning a miss. The exceptions that can reach callers implement `Psr\Cache\CacheException`, and an illegal key raises `Psr\Cache\InvalidArgumentException`. ## Example: warming many rates at once An exchange-rate library can warm its cache for every currency pair after one bulk API call: ```php foreach ($rates as $pair => $rate) { $item = $pool->getItem('rate.' . $pair); $pool->saveDeferred($item->set($rate)->expiresAfter(600)); } if (!$pool->commit()) { $this->logger->warning('Rate cache warm-up was not fully persisted'); } ``` With a backend that supports multi-key writes, this becomes one round trip instead of one per pair. ## Comparison with PSR-16 PSR-16 has no deferred mode. Its batch equivalent is `setMultiple($values, $ttl = null)`, which writes a set of key/value pairs in one call and returns a boolean. That covers the "many writes, one round trip" case, but not incremental queuing across different parts of the code followed by a single flush. ## Checklist - Use `saveDeferred()` for many writes that belong together; `save()` for single writes. - Always finish a batch with `commit()` and check its result. - Treat deferral as batching, not as a transaction.

  • Is PSR-6's saveDeferred() plus commit() a transaction you can roll back?
    No. The pool may persist deferred items before `commit()`, on its own schedule, and PSR-6 defines no rollback method. Deferral exists so the pool can batch writes, not to make a set of writes atomic. Treat `commit()` as a flush whose result you check.
  • How does a PSR-6 caller find out that a cache write failed?
    Through return values: `save()`, `saveDeferred()` and `commit()` return `false` on failure. PSR-6 tells implementations not to throw anything beyond the defined exception interfaces and to trap storage errors, reporting them instead, so an outage usually looks like `false` on write and a miss on read.

saying these in an interview costs you the question

  • Believes nothing reaches storage until commit() is called.
  • Thinks getItem() on a deferred key returns a miss until commit().
  • Treats saveDeferred() and commit() as an atomic transaction with rollback.
  • Expects commit() to throw an exception when a write fails.
  • Relies on the pool's destructor to flush deferred items.