Pluggable Backends
Several winterbaume services separate their protocol handlers from their storage or query-execution engines behind an object-safe async backend trait. The default backend is always in-memory; alternative implementations live in separate crates and are injected at construction time.
Design rules
- The backend trait lives in the service crate so the service owns its protocol-to-backend contract.
- The trait must be object-safe: async methods return
Pin<Box<dyn Future<...> + Send>>. new()creates the service with the default in-memory adapter.with_backend(impl Backend)(orwith_query_backend(...)) is the stable injection point.- Heavy dependencies (Redis, DuckDB, native libs) live in separate crates. Core service crates stay dependency-light.
- When
StatefulServiceis implemented, the backend is the authoritative owner of durable state.snapshot(),restore(), andmerge()delegate through the backend — not through a hidden in-memory shadow.
Storage backends
SQS (SqsBackend)
Defined in crates/winterbaume-sqs/src/backend.rs.
pub trait SqsBackend: Send + Sync + 'static {
async fn create_queue(&self, params: CreateQueueParams) -> Result<QueueInfo>;
async fn get_queue(&self, url: &str) -> Result<Option<QueueInfo>>;
async fn send_message(&self, url: &str, msg: Message) -> Result<String>;
async fn receive_messages(&self, url: &str, max: u32, visibility_timeout: u32)
-> Result<Vec<Message>>;
async fn delete_message(&self, url: &str, receipt_handle: &str) -> Result<()>;
// ... full queue management surface
}In-memory adapter: InMemorySqsBackend (default, in winterbaume-sqs). Redis adapter: RedisSqsBackend (in winterbaume-sqs-redis).
use winterbaume_sqs::SqsService;
use winterbaume_sqs_redis::RedisSqsBackend;
let backend = RedisSqsBackend::new("redis://127.0.0.1/").await?;
let sqs = SqsService::with_backend(Arc::new(backend));DynamoDB (DynamoDbBackend)
Defined in crates/winterbaume-dynamodb/src/backend.rs.
The DynamoDB backend owns item storage, table metadata, and GSI/LSI index state. PartiQL execution runs above the backend via execute_partiql_via_backend(...) — it is not a backend method itself.
use winterbaume_dynamodb::DynamoDbService;
use winterbaume_dynamodb_redis::RedisDynamoDbBackend;
let backend = RedisDynamoDbBackend::from_url("redis://127.0.0.1/").await?;
let ddb = DynamoDbService::with_backend(Arc::new(backend));VFS / BlobStore (S3, EBS, Glacier)
S3, EBS, and Glacier use the Vfs abstraction from winterbaume-core rather than a service-owned backend trait. Pass a shared Arc<dyn Vfs> at construction:
use winterbaume_core::FsVfs;
use winterbaume_s3::S3Service;
use winterbaume_glacier::GlacierService;
use winterbaume_ebs::EbsService;
let vfs = Arc::new(FsVfs::new("/var/lib/winterbaume/blobs")?);
let s3 = S3Service::with_vfs(Arc::clone(&vfs));
let glacier = GlacierService::with_vfs(Arc::clone(&vfs));
let ebs = EbsService::with_vfs(Arc::clone(&vfs));Sharing the Arc<dyn Vfs> is intentional: all three services can coexist in the same storage directory without key collisions because each service namespaces its blob keys internally.
Query-execution backends
Athena (AthenaQueryBackend)
Defined in crates/winterbaume-athena/src/backend.rs.
The default backend returns empty result sets. The DuckDB backend executes real SQL:
use winterbaume_athena::AthenaService;
use winterbaume_sqlengine_duckdb::DuckDbAthenaBackend;
let backend = DuckDbAthenaBackend::new()?;
let athena = AthenaService::with_query_backend(Arc::new(backend));Redshift Data (RedshiftQueryBackend)
Defined in crates/winterbaume-redshiftdata/src/backend.rs. Uses the same DuckDB crate:
use winterbaume_redshiftdata::RedshiftDataService;
use winterbaume_sqlengine_duckdb::DuckDbRedshiftBackend;
let backend = DuckDbRedshiftBackend::new()?;
let redshift_data = RedshiftDataService::with_query_backend(Arc::new(backend));Implementing a new backend
To add a new storage backend for an existing service:
Implement the trait from the service crate. The trait is in
crates/winterbaume-{svc}/src/backend.rs.Create a new crate (e.g.
crates/winterbaume-{svc}-mystore/) with heavy dependencies isolated there. Add it to the workspace[workspace.members]list.Implement the trait methods. All async methods must return
Pin<Box<dyn Future<...> + Send + 'static>>— the object-safety constraint comes from the service crate's trait definition.Implement
StatefulServicedelegation. If the service implementsStatefulService, the backend should implement snapshot, restore, and merge. The service'sStatefulServiceimpl will delegate to the backend rather than to an in-memory copy.Wire into the server (optional). Add a feature flag to
winterbaume-server/Cargo.tomland handle the new backend option inmain.rs, following the SQS/DynamoDB patterns.
State ownership with backends
When a backend is present, it is the single source of truth for durable state:
StatefulService::snapshot()
→ service.backend.snapshot(account, region)
→ backend reads from Redis / DuckDB / etc.
→ returns StateView
StatefulService::restore(view)
→ service.backend.restore(account, region, view)
→ backend writes to Redis / DuckDB / etc.A service that maintains a separate in-memory shadow alongside a backend will have divergent state after a restore. Always route snapshot/restore/merge through the backend.