Two caches can expose the same Get method yet disagree about expiry, returned
bytes, or missing entries. A contract combines a service’s driver interface
with its documented behavioral rules. Those rules tell you what application
code can rely on when selecting or replacing a driver.
Behavioral Guarantees
Read the method documentation as well as its signature. The
cache.Driver contract,
for example, establishes that:
- Zero write TTL means no expiry; a negative TTL fails with
ErrInvalidTTL. - Expired entries behave like missing entries.
Getreports a missing entry withErrNotFound, whileHasreturnsfalseand deleting a missing entry succeeds.- Stored and returned byte slices are independent copies, so caller mutation does not change cached data.
- An already-canceled context prevents the operation and leaves the cache unchanged.
These are distinct promises. A successful read does not imply that an entry never expires, and a successful deletion does not imply that an entry existed. The contract gives each result a meaning your code can use.
The facade adds its own documented behavior. Cache
Get decodes driver bytes into the key’s Go type, so it can fail on invalid
JSON even after the driver successfully finds an entry. That is not a cache
miss; the stored entry remains unchanged.
Error Identity
Classify documented conditions with errors.Is, not an error string. Drivers
can wrap a sentinel with additional context without changing its identity.
Sentinels belong to their service: a cache miss and a missing database row do
not share one universal not-found error.
Suppose store is an existing *cache.Cache, key is a cache.Key[ReportStatus]
as in Typed Service Calls, and ctx
is the operation context. Import errors and
arkoren.dev/core/service/cache to distinguish absence from a failed read:
value, err := store.Get(ctx, key)
switch {
case errors.Is(err, cache.ErrNotFound):
// Load from the authoritative store instead.
case err != nil:
return err
default:
// Use the cached value.
}
Only the missing-entry branch establishes that the cache has no value for this key. A connection failure or decoding error must not silently become a cache miss. Decide explicitly whether your application can continue without caching.
Contracts do not give every possible backend failure a portable sentinel. Preserve unexpected errors for the caller or operational reporting; do not invent a classification from vendor error text.
Context Boundaries
Pass the request or job’s context to I/O operations so cancellation and
deadlines can reach the driver. Use errors.Is(err, context.Canceled) or
errors.Is(err, context.DeadlineExceeded) when cancellation affects your next
decision. Local hashing and encryption APIs do not take a context merely to
match this shape.
Distinguish an already-canceled operation from cancellation during a write.
The cache contract guarantees no mutation for the former. Once a command has
reached a remote backend, an error does not necessarily establish whether the
backend applied it. For example, cache CompareAndPut explicitly documents
that a network error can leave its outcome unknown. A deadline is not a
rollback guarantee.
Atomic Operations
An atomic operation makes a decision and its associated change indivisible
to competing callers. Cache Add, for example, stores a value only if its key
is absent; a rejected addition leaves the existing value and TTL untouched.
Calling Has followed by Put is not equivalent: another caller can write
between the two calls.
When a contract permits ErrUnsupported, a driver unable to meet the required
semantics returns that error instead of an unsafe approximation. Do not
replace an unsupported atomic call with separate operations. Select another
backend or redesign the operation, as explained in
Unsupported Operations.
The portable contract is not a catalog of everything a vendor provides. Native features accessed through a vendor client sit outside those portable guarantees. Likewise, a shared API does not promise identical durability, deployment requirements, or performance across drivers.
Conformance Evidence
A conformance suite exercises a driver’s behavior against its contract.
When writing or adapting a cache driver, call the public cachetest.Run suite
with a driver factory. In a _test.go file, import testing,
arkoren.dev/core/service/cache, and its cachetest and memorycache
subpackages:
func TestCacheContract(t *testing.T) {
cachetest.Run(t, func(t *testing.T) cache.Driver {
return memorycache.New()
})
}
The factory is called for each subtest. The cache suite uses unique keys, so an
external backend need not start empty. It checks expiry, byte copying,
conditional writes, counters, cancellation, and concurrent access. Server-owned
expiry needs the suite’s WithExternalClock option; local expiry can use
simulated time.
Passing the suite is evidence for the tested behavior, not proof about every production configuration or failure mode. The interface documentation remains binding even when a rule has no test. Follow Writing a Driver when implementing a backend, or Conformance Suites when integrating its tests.
Application Tests
A fake records requested effects so an application test can inspect them without a production provider. For example, a queue fake can establish that your code dispatched the expected job through the real facade. It cannot prove that a production broker persists or redelivers that job.
An in-process driver serves a different purpose: it executes the service’s behavior locally. Use the service’s testing guide to choose between recording an effect and executing it. The workflow fake records commands and accepts scripted outcomes without executing workflow bodies. The scheduling fake records work and can explicitly drain due jobs through real handlers; it does not run timers on its own. Use memory drivers when you need workflow execution or automatic timer behavior. Fakes describes those boundaries without requiring backend infrastructure.
For the concrete guarantees used throughout this page, see the
cache API reference.