---
title: Cache product prices
description: Load a product price once, reuse it from Redis, and verify that it loads again after expiration.
pagination: false
sidebar:
  icon: zap
---

Add a product-price lookup to your application. You'll load a price, reuse it for one minute, and verify that it loads again after expiration. This example uses a fixed price, so it needs no database.

> **Before you begin**
>
> Continue from the [Orders endpoint](/docs/learn/first-route), so the Orders service already
> exists. Create the two files below, or update them if they already exist. Run commands from your
> app's root. This selected local profile requires a running Docker daemon and uses Redis.

1. ## 1. Configure caching [#configure-caching]

    Install the local Redis integration from your app root:

    {/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

    From this guide's application/package root, install only its selected requirements:

    ```sh
    bun add @relkit/docker@0.7.12 @relkit/redis@0.8.1
    ```

    Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in `dependencies`; build/type tooling belongs in `devDependencies`. Installing an adapter does not supply its optional SDK peers or enable the feature's configuration.

    In `relkit.config.ts`, import `redis` from `@relkit/redis`, `docker` from `@relkit/docker`, and
    `env as binding` from `@relkit/app/config`. Create a `cache` object inside `defineApp()` if the
    app has none, then add this `requests` entry. It is the only cache profile needed here:

    ```ts title="relkit.config.ts — cache configuration"
    requests: docker(redis({ url: binding.secret("REQUESTS_REDIS_URL") })),
    ```

    The excerpt is from the larger commerce example, which also has a `timeline` profile. Do not add
    that second profile here. `requests` starts a Docker Redis recipe during local development;
    `orders.prices` selects it explicitly. Connected or cloud-owned profiles are covered in
    [Define a cache](/docs/modules/caching/define).

2. ## 2. Define the prices cache [#define-the-prices-cache]

    ```ts title="src/services/orders/cache/prices.cache.ts"
    /** Declares integer SKU prices in the requests cache profile with a one-minute default and five-minute maximum TTL. */
    import { defineCache } from "@relkit/app/cache";
    import { z } from "@relkit/app/schema";

    const prices = defineCache({
      id: "orders.prices",
      profile: "requests",
      key: z.object({ sku: z.string() }),
      value: z.number().int().nonnegative(),

      defaultTtlMs: 60_000,
      maxTtlMs: 300_000,
    });

    export default prices;
    ```

    Each product code has its own cached price, which expires one minute after it is saved.

3. ## 3. Add the lookup function [#add-the-lookup-function]

    ```ts title="src/services/orders/functions/get-price.function.ts"
    /** Exercises the real prices dependency with a logged cache-miss producer and validated SKU/price results. */
    import { defineFunction } from "@relkit/app/functions";
    import { z } from "@relkit/app/schema";
    import prices from "@app/services/orders/cache/prices.cache.js";

    const getPrice = defineFunction({
      id: "orders.get-price",

      input: z.object({ sku: z.string().min(1) }),
      output: z.object({ sku: z.string(), unitPriceCents: z.number().int().nonnegative() }),

      dependencies: { cache: { prices } },

      handler: async ({ sku }, context) => {
        const unitPriceCents = await context.cache.prices.getOrSet({ sku }, async () => {
          // Runs on a cache miss. Replace the demo price with your database lookup.
          context.log.info("price.loaded", { sku });
          return 1_000;
        });

        return { sku, unitPriceCents };
      },
    });

    export default getPrice;
    ```

    The `price.loaded` log runs only when the callback loads a price.

4. ## 4. Expose a price endpoint [#expose-a-price-endpoint]

    In `src/services/orders/service.ts`, import the function and add `getPrice` to the existing
    `functions` object. Keep the service's other members:

    ```ts title="src/services/orders/service.ts — new import"
    import getPrice from "./functions/get-price.function.js";
    ```

    Create `src/routes/prices/route.ts`:

    ```ts title="src/routes/prices/route.ts"
    import { defineServiceRoutes } from "@relkit/app/routes";
    import orders from "@app/services/orders/service.js";

    export const { GET } = defineServiceRoutes(orders, {
      GET: { member: "getPrice" },
    });
    ```

5. ## 5. Try a miss and a hit [#try-a-miss-and-a-hit]

    Check and start the app:

    ```sh
    bun run check
    ```

    ```sh
    bun run dev
    ```

    Docker is contacted only after the checked plan exists. To prove there is no hidden fallback, use
    `bun run dev --local=off`; startup then requires the named connected/infrastructure values.

    In a second terminal, send the same request twice:

    ```sh
    curl http://localhost:3000/prices\?sku=book-demo
    curl http://localhost:3000/prices\?sku=book-demo
    ```

    Expect this output:

    ```json
    { "sku": "book-demo", "unitPriceCents": 1000 }
    ```

    Look for `price.loaded` with `sku=book-demo` in the app terminal. The second request within one minute returns the same output without another `price.loaded` log. Use a new SKU if this key was already cached from an earlier run.

6. ## 6. Inspect expiration [#inspect-expiration]

    Optionally complete [Inspector setup](/docs/modules/operations/inspector). Open **Cache**, select `orders.prices`, and find the key containing `book-demo`. Its remaining TTL is the time before the cached value expires. Click **View value** to see `1000`.

    Once the minute has passed, repeat the curl request with the same SKU. A new `price.loaded` log confirms the price was loaded again. The next request performs the reload; nothing refreshes in the background.

    Tests must pass an explicit replacement for the `requests` profile; no test environment selects
    a fake for you. In your app, replace the fixed price with a database lookup and
    [invalidate the cached price after updates](/docs/modules/caching/expiration). Before deploying, review which
    profiles are connected and which are infrastructure-owned in [Integrations](/docs/modules/integrations).

## Related tasks

[Invalidate prices after an update](/docs/modules/caching/expiration), or [react to order
events](/guides/events/react-to-order-events).
