Protocol > Interop Mode
Language-neutral key format for sharing cache entries across SDK implementations.
Status: DRAFT — See Issue #1 for discussion. This mode is NOT yet implemented in any SDK.
- The Problem
- Interop Key Format
- Interop Value Format
- Canonical Argument Normalization
- Encryption in Interop Mode
- SDK Implementation
The default key format includes language-specific function identity:
- Python: ns:users:func:myapp.services.get_user:args:{hash}:1s
- Go: ns:users:func:services.GetUser:args:{hash}:1sDifferent function paths produce different keys, so Python and Go write to different cache entries even for the same operation and arguments. Cross-SDK cache sharing requires a language-neutral key format.
Note
This problem only affects the func: segment. The args:{hash} segment is identical across languages as long as arguments are canonically normalized (see Canonical Argument Normalization below).
{namespace}:{operation}:{args_hash}
| Segment | Description | Example |
|---|---|---|
namespace |
User-specified cache namespace | users |
operation |
User-specified operation name (language-neutral) | get_user |
args_hash |
Blake2b-256 hex of canonically-serialized arguments | a3c8d4f2... |
No func: segment. No metadata suffix. The operation name is always explicit — it cannot be auto-derived because naming conventions differ across languages.
Caution
Operation names must be agreed on out-of-band across all SDK deployments. If Python uses "get_user" and Go uses "GetUser", they will still produce different keys. Pick a convention and document it in your service.
Plain MessagePack. No ByteStorage envelope, no LZ4 compression, no xxHash3-64 checksum.
Any language with a MessagePack library can read and write interop-mode values. This trades the integrity checking and compression of the standard ByteStorage envelope for maximum portability.
For the args_hash to match across SDKs, arguments must be normalized identically before serialization.
| Source Type | MessagePack Encoding | Normalization Rule |
|---|---|---|
| Integer | msgpack int | Direct encoding |
| Float | msgpack float64 | IEEE 754 double |
| String | msgpack str | UTF-8 encoded |
| Boolean | msgpack bool | Direct encoding |
| Null/None/nil | msgpack nil | Direct encoding |
| List/Array | msgpack array | Preserve element order |
| Dict/Map | msgpack map | Sort keys lexicographically (recursive) |
| Set | msgpack array | Sort elements, encode as array |
| DateTime | msgpack float64 | Convert to UTC Unix timestamp |
| UUID | msgpack str | Lowercase hyphenated: "12345678-1234-..." |
| Bytes | msgpack bin | Raw binary |
Argument encoding: msgpack([positional_args, sorted_kwargs_dict])
Important
Dict keys MUST be sorted lexicographically (not by insertion order). This is the most common interop bug — languages with ordered maps (Python 3.7+, JS) may preserve insertion order by default. The normalization rule overrides that.
Example: get_user(42, include_profile=True) encodes as:
msgpack([[42], {"include_profile": true}])
Encryption works identically to standard mode. The AAD v0x03 format binds to the key string — since interop keys are identical across SDKs, the AAD will also be identical, and cross-SDK encrypted payloads will decrypt successfully.
See encryption.md for the full encryption specification.
The interop API adds an interop parameter to the standard cache decorator / attribute:
# Python
@cache(interop="get_user", namespace="users", ttl=300)
def get_user(user_id: int):
return db.fetch(user_id)// Rust
#[cachekit(interop = "get_user", namespace = "users", ttl = 300)]
fn get_user(user_id: i64) -> User {
db.fetch(user_id)
}// Go
cache.Interop("get_user", "users", 300, GetUser)All three generate the same key: users:get_user:{identical_blake2b_hash}