Deep Dive

Patch-native caching with RFC 7386

JSON Merge Patch as a first-class cache operation, PATCH_ITEM deltas, and the embedded object cache that reassembles full objects locally.

Why it matters

Most caches force a false choice for structured values: either you GET the whole object, mutate a field, and PUT it back — racing every other writer and shipping kilobytes to change a byte — or you shard the object into a hundred tiny keys and lose atomicity. Aeron Cache offers a third option: treat partial update as a native cache operation. A patch merges a fragment into the stored JSON server-side, and subscribers can receive only the fields that changed. For config distribution and pricing fan-out — where one field of a large document ticks constantly — this is the difference between streaming deltas and re-broadcasting the world.

RFC 7386 semantics

Aeron Cache implements JSON Merge Patch (RFC 7386). The rules are small and worth committing to memory:

  • Objects merge recursively. A patch object is applied key by key; nested objects descend rather than replace.
  • Scalars and arrays replace wholesale. There is no element-wise array merge — supply the new array and it overwrites the old.
  • null deletes the field. A key set to null in the patch removes that key from the target entirely.

A worked example. Suppose the cache holds this configuration document:

{
  "service": "pricing",
  "limits": { "rps": 1000, "burst": 200 },
  "regions": ["us-east", "eu-west"],
  "legacyFlag": true
}

Apply this merge patch:

{
  "limits": { "burst": 500 },
  "regions": ["us-east", "eu-west", "ap-south"],
  "legacyFlag": null
}

The result is:

{
  "service": "pricing",
  "limits": { "rps": 1000, "burst": 500 },
  "regions": ["us-east", "eu-west", "ap-south"]
}

All three rules fire at once: limits merged recursively (rps survived untouched, burst changed), regions replaced as a whole array, and legacyFlag — set to null in the patch — was deleted from the document entirely. That deletion-by-null is the rule people forget, and it is exactly what lets a patch remove a feature flag, not just toggle it.

Patch as a cluster operation

A patch is a committed command like any other, which means it is ordered and replicated deterministically across the cluster (see RAFT consensus). In the cluster service, handlePatchValue decodes the request, asks the cache manager to merge the fragment into the stored value, and then — the interesting part — notifies two different kinds of subscriber:

protected <VT extends Reusable> void handlePostPatchValue(I cacheId, K key, VT patch,
        PatchValueResult<I, K, VT> patchValueResult, ClientSession session,
        CacheResponseEncoder<I, K, VT> encoder,
        CacheSubscriptionService<I, K, VT> subscriptionService,
        CacheSubscriptionService<I, K, VT> patchSubscriptionService) {

    if (patchValueResult.getStatus() == CacheOperationStatus.SUCCESS) {
        VT value = patchValueResult.getEntryValue();      // the full, merged object

        // Full subscribers get the complete new value as an ADD_ITEM/update.
        var entryUpdatedLength = encoder.encodeEntryUpdated(key, value, patchEntryUpdateResult, egressBuffer);
        subscriptionService.handleEntryAdded(patchEntryUpdateResult, egressBuffer, key, value, entryUpdatedLength);

        // Patch subscribers get only the delta, as a PATCH_ITEM.
        if (patchSubscriptionService != null) {
            var entryPatchedLength = encoder.encodeEntryPatched(key, patch, patchEntryUpdateResult, egressBuffer);
            patchSubscriptionService.handleEntryAdded(patchEntryUpdateResult, egressBuffer, key, value, entryPatchedLength);
        }
    }
}

So a single patch produces two wire messages tuned to two audiences: full subscribers receive the whole merged object; patch-mode subscribers receive just the fragment, as a PATCH_ITEM event.

PATCH_ITEM events and patch-mode subscriptions

PATCH_ITEM is one of the five update event types in the wire protocol (ADD_ITEM, REMOVE_ITEM, CLEAR_CACHE, DELETE_CACHE, PATCH_ITEM — see Aeron + SBE transport). A patch-mode subscription is one that asks to receive only these deltas, declared via the SubscriptionMode.PATCH flag on subscribe (full details in streaming subscriptions).

There is a nice optimization in the add path, too: the cluster only bothers to compute a merge-patch delta when a patch subscriber is actually listening for that cache and key. No listener, no work:

PatchValueResult<I, K, VT> patchOut =
        (patchSubscriptionService != null && mergePatchOut != null
                && patchSubscriptionService.hasSubscriber(cacheId, key))
                ? mergePatchOut : null;
var addCacheEntryResult = cache.add(key, value, patchOut);

Reassembling the object locally: EmbeddedObjectCache

A delta stream is only useful if the client can turn deltas back into whole objects. That is the job of the embedded object cache. Where the string-valued EmbeddedAeronCache keeps opaque strings — and therefore cannot merge PATCH_ITEM deltas, so it ignores them and rejects patch-mode subscriptions — the EmbeddedObjectCache holds structured JSON and deep-merges every PATCH_ITEM into its local copy using the exact RFC 7386 rules above. Each patch-mode client reconstructs the full object itself, without the untouched fields ever crossing the wire again.

It ships in all four client languages, backed by each ecosystem’s natural JSON type — Java ObjectNode (with getLocalAs to deserialize into a POJO), Python dict, TypeScript Record<string, any>, and Rust serde_json::Value (as EmbeddedObjects). You obtain one with getObjectCache / get_object_cache / embedded_object_cache (see embedded clients).

Why this is a differentiator

For a feature-flag or dynamic-config system, the shape of the problem is one large document read by many services and updated one field at a time. Patch-native caching means a flag flip sends a handful of bytes, the cluster merges it deterministically, and every subscriber’s embedded object cache absorbs the delta with no re-fetch and no lost fields. For market-data fan-out, a price tick is a PATCH_ITEM on the fields that moved — not a full re-publish of the instrument. The combination of server-side merge, delta-only streaming, and local reassembly is what turns a key-value store into a live, structured, patch-native distribution layer.

Takeaway: store whole objects, ship only the diffs, and let each client rebuild the whole locally. RFC 7386 is the contract that makes all three agree.