Database & ORM Compatibility
ORM-Agnostic Design – cache-kit does not depend on ORMs.
It operates on three simple concepts:
- Serializable entities — Any type implementing
CacheEntity - Deterministic cache keys — Consistent identifiers
- Explicit cache boundaries — Clear separation via
CacheFeed
This means:
- ✅ Swap ORMs without changing cache logic
- ✅ Use multiple ORMs in the same application
- ✅ Cache data from any source (DB, API, file system)
Supported ORMs & Database Layers
Tier-1: Recommended (with Examples)
| ORM | Status | Example | Notes |
|---|---|---|---|
| SQLx | ✅ Full Support | actixsqlx | Async-first, compile-time checked SQL |
Tier-1: Compatible (Community Examples Welcome)
| ORM | Status | Example | Notes |
|---|---|---|---|
| SeaORM | ✅ Compatible | Community contributions welcome | Async ORM with migrations |
| Diesel | ✅ Compatible | Community contributions welcome | Mature, type-safe ORM |
| tokio-postgres | ✅ Compatible | Works with any database layer | Pure async PostgreSQL client |
Tier-2: Any Database Layer
cache-kit works with any Rust code that can:
- Fetch entities by ID
- Return
Option<T>(entity or not found) - Implement
DataRepository<T>trait
This includes:
- Custom SQL builders
- NoSQL databases (MongoDB, DynamoDB)
- REST API clients
- File-based storage
- In-memory data structures
Conceptual Flow
Application Code
↓
┌─────────────────────┐
│ cache-kit │ ← Coordinator (framework-agnostic)
└──────────┬──────────┘
│
├─→ Check cache first
│ ↓
│ ┌─────────────────────┐
│ │ Cache Backend │ ← Redis, Memcached, InMemory
│ └─────────────────────┘
│
└─→ If cache miss, fetch from repository
↓
┌─────────────────────┐
│ DataRepository │ ← impl DataRepository<T>
└──────────┬──────────┘
│
↓ Fetch by ID
┌─────────────────────┐
│ Database / ORM │ ← Your choice (SQLx, SeaORM, etc.)
└──────────┬──────────┘
│
↓ Returns
┌─────────────────────┐
│ Domain Entities │ ← impl CacheEntity
└─────────────────────┘
│
↑ (stored in cache)
Key principle: cache-kit coordinates between cache and database. It checks the cache first, and only queries the database (via DataRepository) on cache misses. Domain entities implement CacheEntity and are stored in the cache backend.
SQLx Integration
SQLx is an async, compile-time checked SQL library. It’s the recommended database layer for new projects.
Installation
[dependencies]
cache-kit = { version = "0.9" }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
tokio = { version = "1.41", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
Entity Definition
use serde::{Deserialize, Serialize};
use cache_kit::CacheEntity;
#[derive(Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
pub id: String,
pub username: String,
pub email: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
impl CacheEntity for User {
type Key = String;
fn cache_key(&self) -> Self::Key {
self.id.clone()
}
fn cache_prefix() -> &'static str {
"user"
}
}
Repository Implementation
use cache_kit::DataRepository;
use sqlx::PgPool;
pub struct UserRepository {
pool: PgPool,
}
impl UserRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
pub async fn create(&self, user: &User) -> Result<User, sqlx::Error> {
sqlx::query_as!(
User,
r#"
INSERT INTO users (id, username, email)
VALUES ($1, $2, $3)
RETURNING *
"#,
user.id,
user.username,
user.email
)
.fetch_one(&self.pool)
.await
}
pub async fn update(&self, user: &User) -> Result<User, sqlx::Error> {
sqlx::query_as!(
User,
r#"
UPDATE users
SET username = $2, email = $3
WHERE id = $1
RETURNING *
"#,
user.id,
user.username,
user.email
)
.fetch_one(&self.pool)
.await
}
pub async fn delete(&self, id: &str) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM users WHERE id = $1", id)
.execute(&self.pool)
.await?;
Ok(())
}
}
impl DataRepository<User> for UserRepository {
async fn fetch_by_id(&self, id: &String) -> cache_kit::Result<Option<User>> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1",
id
)
.fetch_optional(&self.pool)
.await
.map_err(|e| cache_kit::Error::RepositoryError(e.to_string()))?;
Ok(user)
}
}
Usage in Service Layer
use cache_kit::{CacheService, CacheFeed, DataRepository, strategy::CacheStrategy};
use cache_kit::backend::InMemoryBackend;
use std::sync::Arc;
pub struct UserService {
cache: CacheService<InMemoryBackend>,
repo: Arc<UserRepository>,
}
impl UserService {
pub async fn get_user(&self, id: &str) -> cache_kit::Result<Option<User>> {
let mut feeder = UserFeeder {
id: id.to_string(),
user: None,
};
self.cache
.execute(&mut feeder, &*self.repo, CacheStrategy::Refresh)
.await?;
Ok(feeder.user)
}
}
Complete examples:
- examples/actixsqlx — Full Actix + SQLx implementation
- examples/actixsqlx/src/services/user_service.rs — Service layer with caching
Database Best Practices
Separate Concerns
✅ Good:
Database Models → Repository → Cache → Service → API
❌ Bad:
Database Models with embedded cache logic
Repository Pattern
Keep repositories focused on data access:
impl UserRepository {
// ✅ Simple, focused data access
pub async fn find_by_id(&self, id: &str) -> Result<Option<User>, DbError> {
sqlx::query_as!(...).await
}
// ❌ Don't mix cache logic in repository
pub async fn find_by_id_cached(&self, id: &str) -> Result<Option<User>, DbError> {
// BAD: Repository shouldn't know about caching
}
}
Error Handling
Convert database errors to cache-kit errors:
impl DataRepository<User> for UserRepository {
fn fetch_by_id(&self, id: &String) -> cache_kit::Result<Option<User>> {
self.internal_fetch(id)
.map_err(|e| cache_kit::Error::RepositoryError(e.to_string()))
}
}
Database Migrations
cache-kit does not handle database migrations. Use your ORM’s migration tools:
SQLx
For SQLx migration setup and usage, please refer to the SQLx documentation.
Next Steps
- Learn about Core Concepts — Understanding cache-kit fundamentals
- Review Async Programming Model — Async-first design
- Explore API Frameworks — Framework integration examples
- See Serialization options — Postcard and serialization
- Review Cache backend choices — Redis, Memcached, InMemory