A sample idempotency middleware for Rust web services, following battle-tested patterns from Stripe and AWS.
Idempotency failures cause duplicate transactions, data corruption, and customer trust issues. Most implementations get it wrong by conflating "duplicate detection" with true idempotency, creating dangerous race conditions.
This library implements the correct atomic patterns to ensure your APIs can be safely retried.
- Client-driven idempotency keys with UUID v4 validation
- Request fingerprinting to prevent key reuse with different payloads
- Atomic lock acquisition to eliminate race conditions
- Concurrent request handling with proper 409 Conflict responses
- Framework integrations: Axum, Actix-Web, Tonic/gRPC
- 24-hour key retention with automatic cleanup
- Response caching for both successes and deterministic failures
Add to your Cargo.toml:
[dependencies]
idempotency-rs = { version = "1.0", features = ["sqlite", "axum-integration"] }use idempotency_rs::{IdempotencyMiddleware, SqliteIdempotencyStore};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
struct CreateOrderRequest {
symbol: String,
quantity: u32,
price: f64,
}
#[derive(Serialize)]
struct CreateOrderResponse {
order_id: String,
status: String,
total: f64,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize store
let store = SqliteIdempotencyStore::new("sqlite:idempotency.db").await?;
let middleware = IdempotencyMiddleware::new(store);
let request = CreateOrderRequest {
symbol: "AAPL".to_string(),
quantity: 100,
price: 150.0,
};
// Process with idempotency protection
let result = middleware.process_request(
Some(Uuid::new_v4().to_string()), // Client-generated key
"user123".to_string(), // User/tenant scoping
"/api/orders".to_string(), // Request path
"POST".to_string(), // HTTP method
&request, // Request payload
|| async { // Your business logic
// Simulate order processing
let order_id = Uuid::new_v4().to_string();
let response = CreateOrderResponse {
order_id,
status: "pending".to_string(),
total: 15000.0,
};
Ok((200, HashMap::new(), response))
},
).await?;
println!("Response: {:?}", result);
Ok(())
}use axum::{Router, routing::post, extract::State, Json};
use idempotency_rs::axum_integration::idempotency_layer;
use std::sync::Arc;
async fn create_order(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateOrderRequest>,
) -> Result<Json<CreateOrderResponse>, StatusCode> {
// Your business logic here
let response = CreateOrderResponse {
order_id: Uuid::new_v4().to_string(),
status: "pending".to_string(),
total: req.quantity as f64 * req.price,
};
Ok(Json(response))
}
struct AppState {
idempotency_store: SqliteIdempotencyStore,
}
#[tokio::main]
async fn main() {
let store = SqliteIdempotencyStore::new("sqlite:idempotency.db")
.await
.expect("Failed to create store");
let state = Arc::new(AppState {
idempotency_store: store,
});
let app = Router::new()
.route("/orders", post(create_order))
.layer(idempotency_layer(state.clone()))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}Start the example server:
cargo run --example axum_server --features axum-integrationTest with curl:
# Generate a UUID for testing
KEY=$(uuidgen)
# First request
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "X-User-ID: testuser" \
-d '{"symbol":"AAPL","quantity":100,"price":150.0}'
# Retry with same key - returns identical response
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "X-User-ID: testuser" \
-d '{"symbol":"AAPL","quantity":100,"price":150.0}'
# Different payload with same key - returns 422 error
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "X-User-ID: testuser" \
-d '{"symbol":"GOOGL","quantity":50,"price":2000.0}'Or run the automated test suite:
# Run all tests
make test
# Run integration tests specifically
cargo test --test integration_tests
# Run with coverage
make coveragePerfect for single-instance deployments and development:
let store = SqliteIdempotencyStore::new("sqlite:./idempotency.db").await?;let middleware = IdempotencyMiddleware::with_config(
store,
Duration::hours(24), // Key retention (TTL)
Duration::seconds(30), // Lock timeout
);- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)