Quick Start: Request Lifecycle
This guide walks you through a complete HTTP request lifecycle with cache-kit, from the moment an HTTP request arrives to when the response is returned. You'll see exactly how cache hits and misses work, when the database is queried, and how the cache automatically populates.
Understanding this flow is essential for effective cache-kit usage in production web services.
What You'll Learn
- How cache-kit integrates with HTTP handlers
- The exact sequence of operations for cache hits vs misses
- Timing expectations for cached vs database queries
- When and how errors are handled
- How to implement this pattern in your own service
Typical Request Flow
Here's what happens when a client requests user data:
Client → HTTP GET /users/123 → Handler → cache.execute() → Response
The cache.execute() call is where the magic happens. Let's break it down step-by-step.
Step-by-Step Walkthrough
Step 1: HTTP Request Arrives
#[get("/users/{id}")]
async fn get_user(
id: web::Path<String>,
cache: web::Data<CacheService<RedisBackend>>,
repo: web::Data<UserRepository>,
) -> Result<Json<User>> {
// Request starts here
The handler receives:
id: The user ID from the URL path ("123")cache: Shared cache service instancerepo: Database repository for fetching users
Step 2: Create Feeder with Entity ID
let mut feeder = UserFeeder {
id: id.into_inner(),
user: None, // Will be populated by cache or DB
};
The UserFeeder holds:
id: The identifier we want to fetchuser: InitiallyNone, will be filled bycache.execute()
Step 3: Execute Cache Operation
cache
.execute(&mut feeder, &*repo, CacheStrategy::Refresh)
.await?;
This is the core operation. Here's what happens inside:
execute() ──→ Does Redis have "user:123"?
├─ YES (Cache Hit) → Deserialize → feed() → feeder.user = Some(user)
│ [~1-2ms total]
│
└─ NO (Cache Miss) → Query database
Serialize with Postcard
Store in Redis (TTL=300s)
feed() → feeder.user = Some(user)
[~10-100ms total]
Step 4: Extract Result
match feeder.user {
Some(user) => Ok(Json(user)),
None => Err(Error::NotFound),
}
}
After execute() completes, feeder.user contains the result:
Some(user): User was found (from cache or DB)None: User doesn't exist in the system
Cache Hit Path
What happens: Redis has the cached entry for "user:123".
Timing: ~1-2ms (Redis network latency + deserialization)
Code flow:
cache.execute()checks Redis for key"user:123"- Redis returns serialized bytes
- cache-kit deserializes using Postcard
- Calls
feeder.feed(Some(user))to populatefeeder.user - Returns to handler
When this happens:
- Second and subsequent requests for the same user ID
- Within the TTL window (default: 5 minutes)
- If cache hasn't been invalidated
Performance benefit:
- Database is NOT queried
- Response time: 1-2ms vs 10-100ms (50x-100x faster)
- Reduced database load
Cache Miss Path
What happens: Redis does NOT have the cached entry for "user:123".
Timing: ~10-100ms (database query + serialization + Redis store)
Code flow:
cache.execute()checks Redis for key"user:123"→ NOT FOUND- Calls
repo.fetch_by_id("123")to query the database - Database returns
Userstruct - cache-kit serializes the user with Postcard
- Stores in Redis with key
"user:123"and TTL - Calls
feeder.feed(Some(user))to populatefeeder.user - Returns to handler
When this happens:
- First request for a user ID
- TTL has expired (e.g., 5 minutes passed)
- Cache was manually invalidated
- Cache service was restarted (if using InMemory backend)
Automatic caching:
The new entry is automatically cached for subsequent requests. No manual cache population needed!
Error Handling
cache-kit uses the Refresh strategy by default, which provides robust error handling:
Scenario 1: Redis is Down
What happens:
cache.execute()attempts to connect to Redis → FAILS- Falls back to database query automatically
- Application continues to work (degraded performance)
Code behavior:
// No code changes needed - Refresh strategy handles this
cache.execute(&mut feeder, &repo, CacheStrategy::Refresh).await?;
Impact:
- All requests hit the database (no caching)
- Response time: 10-100ms (same as cache miss)
- Database load increases
Scenario 2: Database is Down
What happens:
cache.execute()checks Redis → HIT- Returns cached data without touching the database
- Application continues to work normally
Impact:
- Only cached requests succeed
- Requests for new/expired data will fail
- Cache acts as a protective shield for read operations
Scenario 3: Both Redis and Database are Down
What happens:
cache.execute()returns an error- Handler returns HTTP 500 or error response
Mitigation:
For production resilience, see Failure Modes & Resilience for:
- Circuit breaker patterns
- Graceful degradation strategies
- Fallback mechanisms
Complete Example
Here's a full working example you can run:
use actix_web::{web, get, App, HttpServer, Result};
use cache_kit::{
CacheService, CacheEntity, CacheFeed, DataRepository,
backend::RedisBackend,
strategy::CacheStrategy,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize, Debug)]
struct User {
id: String,
name: String,
}
impl CacheEntity for User {
type Key = String;
fn cache_key(&self) -> Self::Key { self.id.clone() }
fn cache_prefix() -> &'static str { "user" }
}
struct UserFeeder {
id: String,
user: Option<User>,
}
impl CacheFeed<User> for UserFeeder {
fn entity_id(&mut self) -> String { self.id.clone() }
fn feed(&mut self, entity: Option<User>) { self.user = entity; }
}
struct UserRepository;
impl DataRepository<User> for UserRepository {
async fn fetch_by_id(&self, id: &String) -> cache_kit::Result<Option<User>> {
// Simulate database query
Ok(Some(User {
id: id.clone(),
name: "Alice".to_string(),
}))
}
}
#[get("/users/{id}")]
async fn get_user(
id: web::Path<String>,
cache: web::Data<CacheService<RedisBackend>>,
repo: web::Data<UserRepository>,
) -> Result<web::Json<User>> {
let mut feeder = UserFeeder {
id: id.into_inner(),
user: None,
};
cache
.execute(&mut feeder, &*repo, CacheStrategy::Refresh)
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
match feeder.user {
Some(user) => Ok(web::Json(user)),
None => Err(actix_web::error::ErrorNotFound("User not found")),
}
}
Test it:
# First request (cache miss, ~10-100ms)
curl http://localhost:8080/users/123
# Second request (cache hit, ~1-2ms)
curl http://localhost:8080/users/123
Next Steps
Now that you understand the request lifecycle, explore:
- Core Concepts — Deep dive into feeders, entities, and cache strategies
- Actix + SQLx Example — Production-ready implementation with real database
- Failure Modes & Resilience — Handle cache and database failures gracefully
- Monitoring & Metrics — Track cache hit rates and latency in production