AnnouncementsIntroducing Arkoren

    Menu

    Facades and Drivers

    Separate application-facing service calls from backend selection, configuration, and ownership.

    What's on this page
    1. Use a typed facade while its driver performs backend operations under a shared contract.
    2. Keep cache entry identity, expiry, and result types consistent across reads and writes.
    3. Review backend capabilities and resource ownership when replacing a driver or accessing its native client.
    On this page

    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.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive