Serialization Support
⚠️ Critical Limitation
Decimal types (rust_decimal::Decimal, bigdecimal::BigDecimal) are NOT supported by Postcard serialization.
If your entities use Decimal fields (common in financial apps), you MUST convert to String or i64 before caching. See Decimal Types Not Supported below.
Serialization as a First-Class Concern
cache-kit treats serialization as a first-class architectural concern.
Serialization determines:
- Storage format in the cache backend
- Performance characteristics (speed, size)
- Type support (which Rust types can be cached)
- Interoperability (can other languages read the cache?)
Supported Formats
Tier-1: Postcard (Recommended)
Postcard is the primary recommended serialization format for cache-kit.
| Feature | Postcard |
|---|---|
| Performance | ⚡ Very fast (10-15x faster than JSON) |
| Size | 📦 Compact (40-50% smaller than JSON) |
| Type safety | ✅ Strong Rust type preservation |
| Determinism | ✅ Same input → same output |
| Language support | ❌ Rust-only |
| Decimal support | ❌ No (see limitations below) |
Why Postcard?
- Optimized for Rust — Zero-copy deserialization where possible
- Explicit versioning — Simple versioning with automatic cache invalidation
- Minimal overhead — Field order matters, no field names stored
- Fast — Designed for embedded and performance-critical systems
Installation
Postcard is included by default:
[dependencies]
cache-kit = { version = "0.9" }
Usage
No explicit configuration needed — cache-kit uses Postcard automatically:
use cache_kit::CacheEntity;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
struct User {
id: String,
name: String,
age: u32,
}
impl CacheEntity for User {
type Key = String;
fn cache_key(&self) -> Self::Key { self.id.clone() }
fn cache_prefix() -> &'static str { "user" }
}
// Serialization to Postcard happens automatically
Tier-2: MessagePack (Planned)
MessagePack will be available as an alternative serialization format.
| Feature | MessagePack (Planned) |
|---|---|
| Performance | ⚡ Fast (4-6x faster than JSON) |
| Size | 📦 Compact (50% smaller than JSON) |
| Type safety | ⚠️ Partial |
| Determinism | ⚠️ Partial (field order varies) |
| Language support | ✅ Many languages |
| Decimal support | ⚠️ Depends on implementation |
Community contributions welcome! Help us add MessagePack support.
Serialization Characteristics
Postcard: Binary, Deterministic
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Product {
id: u64, // 8 bytes (compact)
name: String, // length-prefixed
price: f64, // 8 bytes (IEEE 754)
}
// Serialized format (example):
// [id: 8 bytes][name_len: varint][name: UTF-8 bytes][price: 8 bytes]
Key property: Serializing the same value twice produces identical bytes.
let product1 = Product { id: 123, name: "Widget".to_string(), price: 99.99 };
let product2 = Product { id: 123, name: "Widget".to_string(), price: 99.99 };
let bytes1 = postcard::to_allocvec(&product1)?;
let bytes2 = postcard::to_allocvec(&product2)?;
assert_eq!(bytes1, bytes2); // ✅ Always true
This enables:
- Reliable cache keys based on content
- Deduplication in distributed caches
- Reproducible testing
Known Limitations
Decimal Types Not Supported
Postcard (and many binary formats) do not support arbitrary-precision decimal types out of the box.
Affected types:
rust_decimal::Decimalbigdecimal::BigDecimal- Database
NUMERIC/DECIMALcolumns
Why This Limitation Exists
Binary formats like Postcard serialize types based on their in-memory representation. Decimal types have complex internal structures that don’t map cleanly to portable binary formats.
Workaround Strategies
Strategy 1: Convert to Supported Primitives
Store monetary values as integer cents instead of decimal dollars:
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
struct Product {
id: String,
name: String,
price_cents: i64, // ✅ Store $99.99 as 9999 cents
}
impl Product {
pub fn price_dollars(&self) -> f64 {
self.price_cents as f64 / 100.0
}
pub fn set_price_dollars(&mut self, dollars: f64) {
self.price_cents = (dollars * 100.0).round() as i64;
}
}
Pros:
- ✅ No precision loss for monetary values
- ✅ Fast serialization
- ✅ Compact storage
Cons:
- ❌ Manual conversion needed
- ❌ Limited to representable range of
i64
Strategy 2: String Representation
Store decimals as strings (not recommended for performance):
#[derive(Clone, Serialize, Deserialize)]
struct Product {
id: String,
name: String,
price: String, // "99.99" as string
}
Pros:
- ✅ No precision loss
- ✅ Preserves exact decimal representation
Cons:
- ❌ Slower serialization
- ❌ Larger storage footprint
- ❌ Manual parsing required
Serialization Best Practices
DO
- ✅ Use primitive types where possible (
i64,f64,String) - ✅ Convert decimals to integers (cents) for monetary values
- ✅ Create cache-specific DTOs if needed
- ✅ Document conversion logic clearly
- ✅ Test roundtrip serialization
DON’T
- ❌ Assume all Rust types are serializable
- ❌ Mix database types with cache types without conversion
- ❌ Ignore serialization errors
- ❌ Use
unwrap()on deserialization - ❌ Store sensitive data without encryption
Versioning and Schema Evolution
cache-kit uses explicit versioning for cached data.
Current Approach
cache-kit wraps all cached entries in a versioned envelope:
[MAGIC (4 bytes)] [VERSION (4 bytes)] [POSTCARD PAYLOAD]
- MAGIC:
b"CKIT"— Identifies cache-kit entries - VERSION:
u32— Schema version number - PAYLOAD: Postcard-serialized entity
Version Mismatches
When the schema version changes:
- Old entries rejected — Cannot be deserialized
- Cache miss triggered — Fetch from database
- New entry cached — With updated version
No migration — Cache naturally repopulates with new schema.
Handling Schema Changes
When you modify an entity structure:
// Version 1
struct User {
id: String,
name: String,
}
// Version 2 (added field)
struct User {
id: String,
name: String,
email: Option<String>, // New field
}
What happens:
- Deploy your code with the new entity structure
- Old cache entries will fail to deserialize (treated as cache misses)
- Cache will automatically refetch from database and store with new structure
- No manual intervention needed — cache naturally repopulates
Note: The schema version is managed internally by cache-kit. When deserialization fails due to structure changes, entries are automatically treated as cache misses and refetched.
Troubleshooting
Error: “Serialization failed”
Cause: Entity contains unsupported types (e.g., Decimal)
Solution: Convert to supported primitives
Error: “Version mismatch”
Cause: Cached entry has different schema version
Solution: This is expected after schema changes. Entry will be invalidated and refetched.
Error: “Invalid magic header”
Cause: Cache entry is corrupted or not created by cache-kit
Solution: Clear the cache key manually or let it expire
Next Steps
- Learn about Cache backend options
- Review Core Concepts — Design philosophy and principles
- Explore the Actix + SQLx reference implementation
- Contribute MessagePack support! See CONTRIBUTING.md