A report handler should not need Redis commands to cache a report’s status. Arkoren separates the service your code calls from the backend that stores the data. You select the backend when constructing the service, then pass the application-facing object to its consumers.
Three Roles
A facade is the application-facing service, such as *cache.Cache. It adds
shared behavior around backend operations: the cache facade encodes Go values
as JSON, decodes reads, and records telemetry when enabled.
A driver performs the backend operations. For example, memorycache stores
bytes in the current process, while rediscache sends commands to Redis. A
contract defines the interface and behavioral rules between the
facade and driver, including expiry, copying, atomicity, and errors.
Your handler -> Cache facade -> Cache driver -> Stored bytes
typed values raw operations
This separation does not make every backend equivalent. Portable service calls can remain unchanged when you replace a driver; connectivity, supported operations, sharing, and durability still need review.
Typed Service Calls
For a process-local cache, import arkoren.dev/core/service/cache and
arkoren.dev/core/service/cache/memorycache, then construct the facade:
driver := memorycache.New()
store := cache.New(driver)
The facade uses that driver; constructing another facade does not create a
separate backend unless you also supply a separate driver. Pass store to the
handlers or services that need caching, rather than making them select drivers.
To cache a report status, define the value your application wants to read:
type ReportStatus struct {
Ready bool `json:"ready"`
}
A cache.Key[T] associates a physical entry name with a Go value type and write
policy. Import time and give this entry a ten-minute lifetime:
key := cache.NewKey[ReportStatus]("reports:42:status", func(entry *cache.KeyBuilder) {
entry.TTL(10 * time.Minute)
})
With the operation’s ctx context.Context, write the value through the facade:
err := store.Put(ctx, key, ReportStatus{Ready: true})
On success, the driver holds JSON bytes with the configured expiry. A key with no TTL setting has no expiry; reading it does not renew its lifetime.
The same key determines the read’s result type:
status, err := store.Get(ctx, key)
On success, status is a ReportStatus. Missing and expired entries return an
error matching cache.ErrNotFound; malformed stored JSON returns a decoding
error. The Go type does not create a separate storage namespace: two keys with
the same name address the same entry even if their type arguments differ. Keep
the type consistent for each name; Cache Keys explains key
identity and shared prefixes.
Backend Changes
To move this cache to Redis, use the installation, connection, and lifecycle
instructions in Cache Drivers. The composition boundary
supplies a rediscache.Driver instead of a memorycache.Driver; the Put and
Get calls above stay the same.
An in-memory cache belongs to one process and loses its contents on restart. Redis can provide a shared backend, but its persistence depends on your Redis deployment. Choosing a driver does not migrate existing entries.
Construction is not a connectivity check. The Redis cache constructor performs
no network I/O; Start checks connectivity, and Stop closes its client. Other
drivers have different lifecycle requirements. Register only the methods the
selected driver supplies, under one owner, using
Lifecycle and Ownership.
Several facades can coexist. For example, a service can use one cache for
sessions and another for report status. The cache facade’s Name setting labels
telemetry; it does not isolate entries. Use separate backend configuration or
explicit key prefixes when you need separate data.
Separate Roles
Some services separate the sending and receiving sides of their contract.
Queues have DispatcherDriver and ConsumerDriver; broadcasting has
PublisherDriver and ConsumerDriver. Telemetry has separate trace, metric,
and log driver interfaces.
A web process that only dispatches jobs needs the dispatcher role, not a running consumer. A worker needs the consumer role. One concrete driver can implement both, but the service guide determines which facade and lifecycle each process needs.
Unsupported Operations
A method’s presence does not establish backend support. Contracts can specify
an ErrUnsupported result when a backend cannot provide the required semantics.
Read the service’s driver comparison before depending on such an operation.
For example, cache.Driver.IncrementWithTTL must update the counter and expiry
atomically. A backend that cannot do that returns cache.ErrUnsupported; it
must not substitute separate reads and writes. The rate limiter propagates
that failure rather than counting unsafely. Use errors.Is to classify a
documented sentinel, and do not weaken an atomic operation as a fallback.
Native Client Access
If a required operation exists only in the vendor API, keep that dependency in
a narrow backend-specific part of your application. With a Redis-backed store,
import fmt and arkoren.dev/driver/redis/rediscache to print the native pool’s
current connection count:
if backend, ok := store.DriverAs[*rediscache.Driver](); ok {
fmt.Println(backend.Client().PoolStats().TotalConns)
}
DriverAs finds the requested concrete type, including through wrappers that
expose Unwrap() Driver. It returns false when no match exists. Direct driver
and native-client calls bypass facade behavior, including its telemetry, and
must be reviewed when changing backends.
If you already have a native client, check the driver’s NewFrom ownership
contract before wrapping it. Redis cache’s NewFrom transfers ownership:
Stop closes the supplied client too. Other drivers can borrow clients instead.
Never infer ownership from the constructor name or close a borrowed client
while another component still needs it.
For the facade and accessors used here, see the
cache API reference.