Cache an Answer Without Mixing Callers

Verify tenant-specific cache reuse and invalidation, then distinguish disposable state from business ledgers.

The same retailer asks for the catalogue twice. Repeating the upstream read may be unnecessary for a short period. Another retailer can receive a different price, however, so equal HTTP payloads do not imply equal catalogue requests.

Reusing that answer requires a contract: which requests count as equivalent, how long the answer stays usable, and who may decide it is wrong. Object Store supplies key/value operations and the Cache scope supplies a reuse pattern — the application still has to define that equivalence and lifetime.

Checkpoint 22 uses two synthetic tenants. Retailer A receives PEN-01 at 2.5; retailer B receives it at 3. The caller chooses a fixture tenant in the local diagnostic URL. This is a cache-identity experiment, not an authentication mechanism.

A, A, then B: A price: 2.5: B price: 3. Tenant-aware key: Version + tenant: Upstream uses tenant. Two source calls: A reused; B separate: Invalidate A to refetch. A cache entry is disposable; a business acceptance record is not.

Open this checkpoint in ACB

Stop the previous local application, then use File → Open Folder to open book/checkpoints/22-cache-and-state. Open src/main/mule/app.xml and use Flow List to select the named flow for each example. Start the controlled dependency in a separate terminal from the companion root with python3 book/stubs/server.py. Choose Run and Debug → Run Mule Application and wait for deployment. Save canvas edits and use Save and Hot-deploy to Local Runtime before repeating a request.

Run python3 book/run.py verify 22 from the companion root to exercise this running checkpoint with its synthetic fixtures. The verifier supplies requests and checks results; it does not start the ACB application. Keep the editor on this checkpoint while reading a failure so an old deployment cannot supply a misleading answer.

Put every varying input in the identity

A cache keyed on the SKU alone would hand retailer B the price negotiated by retailer A. The cache is fast, the upstream is healthy, and the answer is wrong because the key omitted an input that changes it. Every input the result depends on therefore belongs in the identity of the request.

Example 059 — Cache a catalogue separately for each tenant

The complete source is in checkpoints/22-cache-and-state/src/main/mule/app.xml.

Choose Flow List → cached-catalogue. The Listener is GET /lab/catalogue/{tenant}. The first Set Variable assigns attributes.uriParams.tenant to tenant. The Choice rejects values outside the two fixture tenants using not (['retailer-a','retailer-b'] contains vars.tenant) and Raise Error APP:INVALID_TENANT.

The next Set Variable assigns 'prices-v1:' ++ vars.tenant to cacheKey. Expand Cache, select its Request, and inspect GET /catalogue through Dependency_HTTP; Query Parameters is expression {tenant: vars.tenant}.

Select the Cache scope and follow its Catalogue_Cache strategy into Global Configurations. Inspect its explicit key expression vars.cacheKey. The private store is already supplied in this checkpoint. In this ACB version the connection panel shows the key but leaves its Object store selector blank for that inline private store; do not interpret the blank selector as a missing runtime store or replace it while following this exercise. The supplied retention settings are described below. The Request must remain inside the Cache scope.

The key combines a representation version and one of the two validated tenant names. The same tenant is also sent to the upstream query. Including tenant only in the key would create separate caches of the same wrong source answer.

The complete configuration uses Catalogue_Cache, an explicit object-store caching strategy with a nonpersistent private store, a ten-minute entry lifetime and a one-minute expiration scan. It holds at most 100 entries in this local backend. These settings do not configure managed Object Store v2.

Call retailer A twice and B once after clearing the cache and resetting the dependency call counter. The bodies must contain the appropriate prices, while the source records two calls. Equal bodies alone could not prove a cache hit; the call counter supplies the missing observation.

The default cache key derives from payload, which is inadequate for GET requests whose identity lives in path, query or trusted caller context. The explicit key avoids that ambiguity. For unrestricted fields, use an unambiguous encoding: naive delimiter joins can make different tuples collide. The two fixed fixture values make this small concatenation safe within the declared experiment.

Cache strategy configuration reading its key from vars.cacheKey.

The strategy reads its key from vars.cacheKey instead of deriving one from the payload. The Set Variable in cached-catalogue is what builds that tenant-aware value.

Invalidate the value that changed

A commercial price correction does not wait ten minutes for an entry to expire. When the source changes, something has to name the answer that is now wrong — for one tenant, ideally, rather than for everything the application has remembered.

Example 060 — Invalidate one tenant catalogue

The complete source is in checkpoints/22-cache-and-state/src/main/mule/app.xml.

Choose Flow List → invalidate-one-tenant. The Listener accepts DELETE /lab/cache/{tenant}. Select Invalidate Key: Caching Strategy is Catalogue_Cache, and Key Generation Expression is 'prices-v1:' ++ attributes.uriParams.tenant. Transform Message returns JSON {invalidated: true}.

Compare this expression with the Set Variable in cached-catalogue; both must construct the same key.

DELETE /lab/cache/retailer-a removes that key. The next A lookup must reach the source again; B can retain its separate entry. The complete checkpoint also provides DELETE /lab/cache using ee:invalidate-cache to clear the strategy for a clean local run.

These are loopback diagnostic routes. A deployed invalidation operation needs authorized access and an audit record, because clearing a cache can direct a surge at the backend — an unauthenticated “clear all” endpoint offered for convenience is a load attack waiting to be used. Targeted invalidation is also easier to reason about than erasing unrelated tokens, cursors and product answers together.

A TTL bounds tolerated age — it does not know when a source price changes. Declare whether ten minutes of stale data is acceptable. If updates must appear immediately, establish how invalidation reaches every relevant instance. Independent local caches can diverge across replicas.

Store a small value explicitly

Not everything an application remembers is a cached response. A note, a cursor or a token is written and read by name, and the application decides both when it appears and what its absence means.

Example 061 — Store and retrieve a local note

The complete source is in checkpoints/22-cache-and-state/src/main/mule/app.xml.

Choose Flow List → store-note. Its HTTP Listener accepts POST /lab/store. Select Store: Object Store Small_Store, Key note, Value expression payload. The next Retrieve uses the same store and key. Transform Message after Retrieve serializes payload as application/json.

Inspect Small_Store in Global Configurations separately from the cache’s private store. Sharing the Object Store connector does not mean these two exercises share their lifecycle.

Object Store provides operations for storing, retrieving, checking and removing small values. In this example Retrieve replaces payload, and the final transform serializes the result as JSON. Use a target when later work needs to retain the incoming message.

A missing key and an unavailable backend are different conditions — the default value should catch only the absence case. A default that also swallows an outage turns a failing store into a cache miss that makes every replica call the upstream at once. The local note is disposable; a request-result ledger or synchronization cursor requires a different retention and failure contract. Absence means different things to those two — a cursor that treats its own disappearance as a first run will replay the whole source, so distinguish initialization from the unexpected loss of a position that was previously established.

Keep recomputation separate from business acceptance

A response cache usually uses time-based eviction and allows recomputation: it may evict an answer precisely because that answer can be produced again. The request-result ledger in chapter 18 remembers a business operation whose side effect must not be repeated merely because a timer expired — a repeated request payload is no reason to create a second order. Caching POST acceptance would erase that distinction.

An error handler inside a cache scope can convert failure into a normal fallback object. That object may then be eligible for caching. Decide explicitly whether not-found results, placeholders or fallback prices may be reused, and retain their meaning in the response. A temporary catalogue outage must not silently become an authoritative zero price.

Object Store operations do not make a multi-operation workflow atomic. Two callers can both observe absence, both call a remote system and both store a marker. A per-key store operation or local lock does not extend across those calls or automatically coordinate independent CloudHub replicas. What to do about that depends on the state: duplicate concurrent computation of an inexpensive catalogue lookup may be acceptable, while a credential refresh or a one-time business effect needs a single owner. Token refresh and synchronization need the additional ownership mechanisms in appendix D.

Try it

1. Detect a cross-tenant leak. Which three requests and observations establish this local cache experiment?

Show answer

Call A twice and B once after reset. Assert A’s 2.5 price, B’s 3 price and two upstream calls. Then invalidate A and require another upstream call on the next A read.

2. Lose the cache. Why may an empty cache trigger more load after a restart?

Show answer

Every arriving lookup may miss before useful entries are rebuilt. Bound aggregate concurrency and test cold behavior; a warm hit ratio says little about the first minute after restart.

3. Forget an order marker. Why can the note store not replace the idempotency transaction?

Show answer

It does not atomically commit the request result with the business order, and its disposable retention permits forgetting. The database constraint and transaction establish a different contract from cached state.

Tenant was a controlled input in this experiment. Before the order service is exposed beyond the local lab, the next chapter makes caller identity and order ownership explicit.

Next: Know Who May Read the Order

Comments