Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Macros

HydraCache macros remove repetition around cache boundaries without making the boundary implicit.

They are useful after the key, tags, TTL, and loader shape are already clear. If those choices are still being designed, start with the explicit runtime APIs first.

Function Wrappers

Use cacheable_loader! when one call site has a fallible async loader and the cache metadata belongs next to the call.

Use cacheable_infallible! when the loader returns a value directly. The cache operation can still fail because serialization, storage, or runtime boundaries can fail.

    let user_id = 42_i64;
    let user = cacheable_loader!(
        cache = cache,
        key = "user:42",
        tags = ["user:42", "users"],
        ttl_secs = 60,
        load = move || async move {
            Ok::<_, LoadError>(User {
                id: user_id,
                name: "Ada".to_owned(),
            })
        },
    )
    .await?;

    assert_eq!(user.name, "Ada");

    let count = cacheable_infallible!(
        cache = cache,
        key = "users:count",
        tags = ["users"],
        ttl_secs = 30,
        load = || async { 1_u64 },
    )
    .await?;

    assert_eq!(count, 1);

Use #[cacheable] when the cached operation is naturally a reusable async function.

#[cacheable(
    cache = cache,
    key_segments = ["profile", user_id],
    tag_segments = [["user", user_id], ["users"]],
    ttl_secs = 60
)]
async fn load_profile(cache: &HydraCache, user_id: i64) -> Result<User, LoadError> {
    Ok(User {
        id: user_id,
        name: "Ada".to_owned(),
    })
}

The cache remains an explicit argument. HydraCache does not discover a global cache and does not derive keys from every function argument.

Entity Metadata

Use HydraCacheEntity when repository code repeatedly caches entity-shaped values.

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, HydraCacheEntity)]
#[hydracache(entity = "user", collection = "users")]
struct User {
    #[hydracache(id)]
    id: i64,
    name: String,
}

fn assert_user_entity_contract() {
    assert_eq!(User::cache_key_for(&42), "user:42");
    assert_eq!(User::entity_tag_for(&42), "user:42");
    assert_eq!(User::collection_tag(), Some("users".to_owned()));
}

The derive produces the CacheEntity metadata used by DbCache, QueryCachePolicy, prepared policies, and invalidation plans:

  • entity key: user:42;
  • entity tag: user:42;
  • optional collection tag: users.

Use #[hydracache(id = Type)] on the struct when the id type is generated or not represented by one named field.

Query Policies

Use query_cache_policy! when one query call site should declare the whole key/tag/freshness contract in one expression.

    let policy = query_cache_policy!(
        preset = read_mostly,
        name = "load-user",
        entity = User,
        id = user_id,
        refresh_ahead_secs = 10,
        stale_while_revalidate_secs = 300,
    );

    assert_eq!(policy.key_value(), Some("user:42"));
    assert_eq!(
        policy.tags_value(),
        &["user:42".to_owned(), "users".to_owned()]
    );

The macro does not inspect SQL. It only builds the same QueryCachePolicy that could be written with builder calls.

Use prepared_query_policy! when most metadata is stable and only the entity id changes at call time.

    let queries = DbCache::new(cache.clone(), "db");
    let load_user = queries.prepare::<User>(prepared_query_policy!(
        per_entity = User,
        name = "load-user",
        ttl_secs = 300,
    ));

    let cached = load_user
        .load_id(42, || async {
            Ok::<_, std::io::Error>(User {
                id: 42,
                name: "Ada".to_owned(),
            })
        })
        .await?;

    assert_eq!(cached.id, 42);

Prepared policies keep hot repository methods compact while preserving an explicit dynamic id boundary.

Write-side Invalidation

The entity derive also keeps write-side invalidation small.

    let pending = InvalidationPlan::new().cache_entity::<User>(42);
    let report = pending.execute(&cache).await?;

    assert_eq!(report.tag_count, 2);

Stage invalidations while preparing repository work, then execute the plan only after the database transaction commits.

Choosing A Macro

MacroUse when
cacheable_loader!One fallible async loader needs local cache metadata.
cacheable_infallible!One infallible async loader should avoid Ok::<_, E>(value) ceremony.
#[cacheable]A reusable async function has stable key/tag metadata.
HydraCacheEntityEntity keys and invalidation tags repeat across repository code.
query_cache_policy!One query call site should declare key, tags, TTL, and refresh policy compactly.
prepared_query_policy!A repository method reuses stable policy metadata and binds only ids or segments per call.

Prefer macro inputs that read like the cache contract a reviewer should approve. If a macro invocation becomes hard to review, move some metadata into named builders or prepared policies.