@periodic/vanadium
v1.0.0
Published
Deterministic idempotency and distributed lock engine for backend systems
Downloads
6
Maintainers
Readme
⬡ Periodic Vanadium
Production-grade, deterministic idempotency and distributed lock engine for Node.js with TypeScript support
Part of the Periodic series of Node.js packages by Uday Thakur.
💡 Why Vanadium?
Vanadium gets its name from the chemical element renowned for its role as a stabilizing agent — added to steel to prevent structural failure under repeated stress. Just like vanadium strengthens metal against fatigue, this library strengthens your backend against the failures that come from calling the same operation more than once.
In chemistry, vanadium is a redox workhorse — capable of holding multiple oxidation states, switching between them reliably and reversibly. Similarly, @periodic/vanadium manages execution state transitions with the same precision: from IN_PROGRESS to COMPLETED, from COMPLETED back to a cached result, from a crashed execution to a safe takeover.
The name represents:
- Stability: Guarantees single-execution semantics under any retry pressure
- Resilience: Survives crashes, restarts, and concurrent callers without corruption
- Precision: Deterministic state transitions with no ambiguity at the boundaries
- Clarity: Explains why an execution was skipped, not just that it was
Just as vanadium is the hidden ingredient that makes critical infrastructure hold together, @periodic/vanadium is the execution primitive that makes your critical operations safe to call more than once.
🎯 Why Choose Vanadium?
In distributed systems, the same operation can be triggered multiple times — and most backends have no defense against it:
- Network retries silently re-submit requests that already succeeded
- Message queues deliver events at-least-once, never exactly-once
- Webhook providers re-send on timeout, no matter what happened the first time
- UI double-submit fires before the first response arrives
- Cron overlap starts two workers on the same job simultaneously
- Crash recovery replays operations that were mid-execution when a process died
Without idempotency primitives, each of these scenarios produces duplicate charges, duplicate emails, duplicate records, or corrupted state. The bug is invisible until it hits production.
Periodic Vanadium provides the perfect solution:
✅ Zero dependencies — Pure TypeScript core, adapters are opt-in
✅ Framework-agnostic — Works with Express, Fastify, or no framework at all
✅ Idempotency Engine — Guarantees a function executes exactly once per key
✅ Distributed Lock Engine — Mutual exclusion across processes and machines
✅ 6 Storage Adapters — Memory, Redis, PostgreSQL, MongoDB, Mongoose, Prisma
✅ Circuit Breaker — Protects against storage failures cascading into outages
✅ HTTP Middleware — Drop-in Express and Fastify support
✅ Crash Recovery — Safely retakes expired IN_PROGRESS records
✅ Payload Hashing — Detects mismatched retries with the wrong parameters
✅ Lifecycle Hooks — Observable without mutating state
✅ OpenTelemetry — Built-in OTEL metrics exporter
✅ Type-safe — Strict TypeScript from the ground up
✅ No global state — No side effects on import
✅ Production-ready — Non-blocking, never crashes your app
📦 Installation
npm install @periodic/vanadiumOr with yarn:
yarn add @periodic/vanadiumOptional peer dependencies (install only what you need):
# Storage adapters
npm install redis # For Redis
npm install pg # For PostgreSQL
npm install mongodb # For MongoDB
npm install mongoose # For Mongoose
npm install @prisma/client # For Prisma
# Exporters
npm install @opentelemetry/api # For OpenTelemetry🚀 Quick Start
import { createIdempotency, createMemoryAdapter } from '@periodic/vanadium';
// 1. Create an idempotency engine
const idempotency = createIdempotency({
adapter: createMemoryAdapter(),
ttlMs: 86_400_000, // cache completed results for 24 hours
});
// 2. Wrap any critical operation
const result = await idempotency.execute('payment:order_123', async () => {
return chargeCard({ amount: 100 });
});
// 3. Call it again with the same key — fn never runs a second time
const same = await idempotency.execute('payment:order_123', async () => {
return chargeCard({ amount: 100 }); // skipped — returns cached result
});Example event output:
{
"key": "payment:order_123",
"status": "COMPLETED",
"result": { "chargeId": "ch_abc123", "status": "succeeded" },
"attempts": 1,
"createdAt": 1708000000000,
"updatedAt": 1708000000312,
"expiresAt": 1708086400000
}🧠 Core Concepts
The createIdempotency Function
createIdempotencyis the primary factory function- Returns a configured idempotency engine instance
- Accepts a storage adapter and flexible configuration options
- This is the main entry point for idempotent execution
- No global state, safe for multi-tenant apps
Typical usage:
- Application code creates an engine with
createIdempotency() - Critical operations are wrapped with
idempotency.execute(key, fn) - Duplicate calls with the same key return the cached result immediately
- Lifecycle hooks and metrics give full observability into every execution
const idempotency = createIdempotency({
adapter: createRedisAdapter({ client }),
ttlMs: 86_400_000,
inProgressExpiryMs: 300_000,
hashPayload: true,
hooks: {
onDuplicateHit: (ctx) => logger.info('duplicate deflected', ctx),
onTakeover: (ctx) => logger.warn('crash recovery takeover', ctx),
},
});The createLock Function
createLockis the factory for distributed mutual exclusion- Guarantees only one caller executes a block at a time, across processes
- Locks auto-expire after
ttlMs— no permanent deadlocks - Safe release is enforced via owner tokens — non-owners cannot unlock
const lock = createLock({
adapter: createRedisAdapter({ client }),
ttlMs: 10_000,
maxWaitMs: 5_000,
});
await lock.acquire('inventory:prod_001', async () => {
await updateInventory('prod_001'); // only one caller at a time
});Execution Lifecycle
Design principle:
Same key → same result, always. The function runs once, the result lives forever (until TTL). Everything else is just a cache hit.
First call → Write IN_PROGRESS → Execute fn → Write COMPLETED → Return result
Duplicate call → Find COMPLETED → Return cached result (fn never called)
Concurrent call → Find IN_PROGRESS (not expired) → Throw VanadiumError(IN_PROGRESS)
Crash recovery → Find IN_PROGRESS (expired) → Atomic takeover → Re-execute fn✨ Features
🔁 Idempotency Engine
Guarantee a function executes exactly once per key, no matter how many times it's called:
const idempotency = createIdempotency({
adapter: createMemoryAdapter(),
ttlMs: 86_400_000,
inProgressExpiryMs: 300_000, // allow crash takeover after 5 minutes
hashPayload: true, // detect mismatched retries
cacheFailures: false, // re-execute on failure by default
});
const result = await idempotency.execute(
'payment:order_123',
async () => chargeCard(),
{ amount: 100, currency: 'USD' }, // payload hash — mismatched retry = error
);🔒 Distributed Lock Engine
Mutual exclusion across processes — safe under 100+ simultaneous callers:
const lock = createLock({
adapter: createRedisAdapter({ client }),
ttlMs: 10_000, // auto-expire after 10s (deadlock protection)
maxWaitMs: 5_000, // wait up to 5s before failing
retryIntervalMs: 50, // check every 50ms while waiting
});
const result = await lock.acquire('inventory:prod_001', async () => {
return updateInventory('prod_001');
});🗄️ Storage Adapters
Six adapters, one interface — behavior is identical across all backends:
// In-memory (zero dependencies — dev, test, single-process)
createMemoryAdapter({ maxKeys: 50_000 })
// Redis (recommended for production)
createRedisAdapter({ client, keyPrefix: 'vanadium:', useLua: true })
// PostgreSQL
createPostgresAdapter({ client: pool, tableName: 'vanadium_records' })
// MongoDB
createMongoAdapter({ client, dbName: 'myapp', useTransactions: true })
// Mongoose
createMongooseAdapter({ model: VanadiumRecord, useTransactions: true })
// Prisma
createPrismaAdapter({ prisma, modelName: 'vanadiumRecord' })🛡️ Circuit Breaker
Protect your app from storage failures cascading into full outages:
import { createCircuitBreaker } from '@periodic/vanadium';
const protectedAdapter = createCircuitBreaker(redisAdapter, {
failureThreshold: 5, // open after 5 consecutive failures
resetTimeoutMs: 30_000, // probe again after 30s
});
const idempotency = createIdempotency({ adapter: protectedAdapter, ttlMs: 60_000 });States: CLOSED (normal) → OPEN (all calls fail immediately) → HALF_OPEN (one probe) → CLOSED
🌐 HTTP Middleware
Drop-in idempotency for Express and Fastify routes:
// Express
app.post('/payments', vanadiumMiddleware(idempotency), async (req, res) => {
const result = await processPayment(req.body);
res.json(result);
});
// Fastify
await app.register(vanadiumFastifyPlugin, { idempotency });Client usage:
POST /payments HTTP/1.1
Idempotency-Key: payment-attempt-uuid-here
Content-Type: application/jsonFirst request executes the handler and caches the response. Every duplicate request with the same key gets the cached response — handler never runs again.
🪝 Lifecycle Hooks
Hook into execution events for observability without mutating state:
const idempotency = createIdempotency({
adapter,
ttlMs: 60_000,
hooks: {
onBeforeExecute: async (ctx) => logger.info('executing', { key: ctx.key }),
onAfterExecute: async (ctx) => logger.info('completed', { key: ctx.key, durationMs: ctx.durationMs }),
onDuplicateHit: async (ctx) => logger.info('duplicate deflected', { key: ctx.key }),
onTakeover: async (ctx) => logger.warn('crash recovery', { key: ctx.key, attempts: ctx.attempts }),
onStorageError: async (err, key) => Sentry.captureException(err, { extra: { key } }),
},
});📊 Metrics
Per-instance metrics, never global:
const metrics = idempotency.getMetrics();
// {
// totalExecutions: 42,
// totalDuplicates: 15,
// totalTakeovers: 2,
// totalStorageErrors: 0,
// inProgressCount: 0,
// totalPayloadMismatches: 1,
// totalFailuresCached: 0,
// }
idempotency.resetMetrics();📡 OpenTelemetry
Built-in OTEL metrics without hard-requiring the SDK:
import { createOtelExporter } from '@periodic/vanadium';
const idempotency = createIdempotency({
adapter,
ttlMs: 60_000,
hooks: createVanadiumMetrics(metrics.getMeter('my-service')),
});📚 Common Patterns
1. Payment Processing
app.post('/payments', vanadiumMiddleware(idempotency), async (req, res) => {
const charge = await idempotency.execute(
`payment:${req.headers['idempotency-key']}`,
async () => stripe.charges.create(req.body),
req.body, // hash payload — catches mismatched retries
);
res.json({ chargeId: charge.id, status: charge.status });
});2. Webhook Deduplication
app.post('/webhooks/stripe', async (req, res) => {
await idempotency.execute(`stripe:${req.body.id}`, async () => {
const event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], secret);
switch (event.type) {
case 'payment_intent.succeeded': await fulfillOrder(event.data.object); break;
case 'customer.subscription.deleted': await cancelSubscription(event.data.object); break;
}
});
res.sendStatus(200);
});3. Distributed Cron Lock
async function runDailyReport(): Promise<void> {
await lock.acquire('cron:daily-report', async () => {
const today = new Date().toISOString().split('T')[0];
await idempotency.execute(`report:${today}`, async () => {
await generateAndSendDailyReport();
});
});
}4. Crash-Safe Job Runner
const jobRunner = createIdempotency({
adapter: redisAdapter,
ttlMs: 7 * 24 * 60 * 60 * 1000, // cache results for 7 days
inProgressExpiryMs: 10 * 60 * 1000, // allow takeover after 10 minutes
});
async function processJob(jobId: string): Promise<void> {
await jobRunner.execute(`job:${jobId}`, async () => {
await runHeavyComputation(jobId);
});
}5. Inventory Update with Lock
const inventoryLock = createLock({
adapter: redisAdapter,
ttlMs: 5_000,
maxWaitMs: 3_000,
retryIntervalMs: 100,
});
async function reserveInventory(productId: string, quantity: number): Promise<boolean> {
return inventoryLock.acquire(`inventory:${productId}`, async () => {
const current = await db.inventory.findOne({ productId });
if (current.stock < quantity) return false;
await db.inventory.update({ productId }, { $inc: { stock: -quantity } });
return true;
});
}6. Double Submit Protection
// Assign a UUID to every form on load, send as Idempotency-Key on submit
app.post('/orders', async (req, res) => {
const formId = req.headers['idempotency-key'];
if (!formId) return res.status(400).json({ error: 'Missing Idempotency-Key' });
const order = await idempotency.execute(`form-submit:${formId}`, async () => {
return createOrder(req.body);
});
res.status(201).json(order);
});7. Severity-Based Error Routing
const idempotency = createIdempotency({
adapter,
ttlMs: 60_000,
hooks: {
onStorageError: async (err, key) => {
Sentry.captureException(err, { extra: { key } });
},
onTakeover: async (ctx) => {
sendToSlack(`⚠️ Crash recovery on ${ctx.key} — attempt ${ctx.attempts}`);
},
},
});8. Production Configuration
import { createIdempotency, createLock, createRedisAdapter, createCircuitBreaker } from '@periodic/vanadium';
const isDevelopment = process.env.NODE_ENV === 'development';
const adapter = isDevelopment
? createMemoryAdapter()
: createCircuitBreaker(
createRedisAdapter({ client: redis, keyPrefix: 'vanadium:', useLua: true }),
{ failureThreshold: 5, resetTimeoutMs: 30_000 },
);
export const idempotency = createIdempotency({
adapter,
ttlMs: 86_400_000,
inProgressExpiryMs: 300_000,
hashPayload: !isDevelopment,
hooks: {
onAfterExecute: (ctx) => logger.info('vanadium.execute', ctx),
onDuplicateHit: (ctx) => logger.info('vanadium.duplicate', ctx),
onStorageError: (err, key) => logger.error('vanadium.storage_error', { err, key }),
},
});
export const lock = createLock({
adapter,
ttlMs: 10_000,
maxWaitMs: isDevelopment ? 0 : 5_000,
});
export default idempotency;🎛️ Configuration Options
createIdempotency Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| adapter | StorageAdapter | required | Storage backend |
| ttlMs | number | 86_400_000 | Completed result TTL (24h) |
| inProgressExpiryMs | number | 300_000 | IN_PROGRESS expiry before crash takeover (5m) |
| hashPayload | boolean | false | Enable payload hash mismatch detection |
| cacheFailures | boolean | false | Cache thrown errors (prevents re-execution on failure) |
| clock | () => number | Date.now | Injectable clock for deterministic testing |
| onDuplicate | (ctx) => void | — | Shorthand callback on duplicate detection |
| hooks | IdempotencyHooks | — | Full lifecycle hook object |
createLock Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| adapter | StorageAdapter | required | Storage backend |
| ttlMs | number | required | Lock TTL — auto-expires to prevent deadlocks |
| retryIntervalMs | number | 50 | How often to retry when waiting for a lock |
| maxWaitMs | number | 0 | Max wait time (0 = fail immediately if locked) |
| clock | () => number | Date.now | Injectable clock for deterministic testing |
| hooks | LockHooks | — | Lifecycle hook object |
createMemoryAdapter Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| maxKeys | number | Infinity | LRU eviction threshold |
| clock | () => number | Date.now | Injectable clock for testing |
createCircuitBreaker Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| failureThreshold | number | 5 | Consecutive failures before OPEN |
| resetTimeoutMs | number | 30_000 | Time in OPEN before HALF_OPEN probe |
| halfOpenMaxCalls | number | 1 | Max probe calls in HALF_OPEN state |
📋 API Reference
Idempotency
createIdempotency(options: IdempotencyOptions): IdempotencyEngine
idempotency.execute(key: string, fn: () => Promise<T>, payload?: unknown): Promise<T>
idempotency.getMetrics(): VanadiumMetrics
idempotency.resetMetrics(): voidLocks
createLock(options: LockOptions): LockEngine
lock.acquire(key: string, fn: () => Promise<T>): Promise<T>
lock.getMetrics(): VanadiumMetricsStorage Adapters
createMemoryAdapter(options?): MemoryAdapter
createRedisAdapter(options): RedisAdapter
createPostgresAdapter(options): PostgresAdapter
createMongoAdapter(options): MongoAdapter
createMongooseAdapter(options): MongooseAdapter
createPrismaAdapter(options): PrismaAdapter
createCircuitBreaker(adapter, options?): CircuitBreakerAdapterHTTP Middleware
vanadiumMiddleware(idempotency: IdempotencyEngine, options?): RequestHandler // Express
vanadiumFastifyPlugin(idempotency: IdempotencyEngine, options?): FastifyPlugin // FastifyError Handling
isVanadiumError(err: unknown): err is VanadiumError
// err.type values:
'DUPLICATE_EXECUTION'
'IN_PROGRESS'
'LOCK_ACQUISITION_FAILED'
'LOCK_TIMEOUT'
'PAYLOAD_MISMATCH'
'CONFIGURATION_ERROR'
'STORAGE_ERROR'
'STATE_TRANSITION_ERROR'Event Structure
interface StoredRecord<T = unknown> {
key: string;
status: 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
result?: T;
payloadHash?: string;
ownerToken?: string;
attempts: number;
createdAt: number;
updatedAt: number;
expiresAt?: number;
}🧩 Architecture
@periodic/vanadium/
├── src/
│ ├── types/
│ │ └── index.ts # All shared type definitions (StoredRecord, StorageAdapter, etc.)
│ ├── errors/
│ │ └── index.ts # VanadiumError class + all factory functions
│ ├── core/
│ │ ├── stateMachine.ts # Valid state transitions (IN_PROGRESS→COMPLETED|FAILED)
│ │ ├── metrics.ts # MetricsStore — per-instance counters
│ │ └── concurrencyGuard.ts # In-process deduplication (local optimization layer)
│ ├── idempotency/
│ │ └── engine.ts # IdempotencyEngineImpl + createIdempotency()
│ ├── lock/
│ │ └── engine.ts # LockEngineImpl + createLock()
│ ├── adapters/ # Storage adapter implementations
│ │ ├── memory/index.ts # MemoryAdapter (built-in, LRU, TTL, CAS, zero deps)
│ │ ├── redis/index.ts # RedisAdapter (Lua CAS, atomic ops)
│ │ ├── postgres/index.ts # PostgresAdapter (advisory locks)
│ │ ├── mongodb/index.ts # MongoAdapter (findOneAndUpdate CAS)
│ │ ├── mongoose/index.ts # MongooseAdapter
│ │ └── prisma/index.ts # PrismaAdapter
│ ├── resilience/
│ │ └── circuitBreaker.ts # CircuitBreakerAdapter (CLOSED/OPEN/HALF_OPEN)
│ ├── cleanup/
│ │ └── engine.ts # CleanupEngine (background stale record cleanup)
│ ├── http/
│ │ ├── express.ts # Express middleware
│ │ └── fastify.ts # Fastify plugin
│ ├── observability/
│ │ └── metrics.ts # OTel-compatible metrics
│ └── utils/
│ ├── crypto.ts # SHA-256 hashing, UUID token generation
│ ├── keys.ts # Key validation and namespacing
│ └── sleep.ts # Non-blocking sleep + jitterDesign Philosophy:
- Core is pure TypeScript with no dependencies
- Adapters implement a single
StorageAdapterinterface — swap without changing application code - HTTP middleware is thin — it delegates entirely to the idempotency engine
- Circuit breaker wraps any adapter — composable, not built-in
- Hooks are observer-only — they can never affect execution outcome
- Easy to extend with custom adapters
📈 Performance
Vanadium is optimized for production workloads:
- Zero blocking — All storage operations are async, never delay response
- In-process coalescing — Concurrent calls for the same key within one process are deduplicated before hitting storage
- LRU eviction — Memory adapter is bounded and never grows unbounded
- Lua scripts — Redis CAS is atomic at the server, no round-trip races
- Hook isolation — Hook errors are silently swallowed, never affect execution
- No monkey-patching — Clean hooks only, no prototype mutation
🚫 Explicit Non-Goals
This package intentionally does not include:
❌ Message queuing (use BullMQ, RabbitMQ, or Kafka)
❌ Job scheduling (use cron libraries or cloud schedulers)
❌ Distributed consensus (use etcd or ZooKeeper)
❌ Retry logic (it prevents redundant retries, not manages them)
❌ Business data storage (it stores execution state, not your data)
❌ Built-in dashboards (use Grafana, Datadog, etc.)
❌ Blocking behavior in production
❌ Magic or implicit behavior on import
❌ Configuration files (configure in code)
Focus on doing one thing well: deterministic, safe, single-execution semantics for critical operations.
🎨 TypeScript Support
Full TypeScript support with complete type safety:
import type {
StorageAdapter,
StoredRecord,
IdempotencyOptions,
LockOptions,
VanadiumMetrics,
VanadiumErrorType,
IdempotencyHooks,
LockHooks,
} from '@periodic/vanadium';
// Fully generic — type inference works automatically
const result: string = await idempotency.execute('key', async () => 'hello');
// Explicit generic when needed
const record: { id: number } = await idempotency.execute<{ id: number }>(
'key',
async () => ({ id: 42 }),
);🧪 Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchNote: All tests achieve >80% code coverage.
🤝 Related Packages
Part of the Periodic series by Uday Thakur:
- @periodic/iridium - Structured logging
- @periodic/arsenic - Semantic runtime monitoring
- @periodic/zirconium - Environment configuration
- @periodic/obsidian - HTTP error handling
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
Build complete, production-ready APIs with the Periodic series!
📖 Documentation
🛠️ Production Recommendations
Environment Variables
NODE_ENV=production
REDIS_URL=redis://...Log Aggregation
Pair with @periodic/iridium for structured JSON output:
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
import { createIdempotency } from '@periodic/vanadium';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
const idempotency = createIdempotency({
adapter,
ttlMs: 86_400_000,
hooks: {
onAfterExecute: (ctx) => logger.info('vanadium.execute', ctx),
onDuplicateHit: (ctx) => logger.info('vanadium.duplicate', ctx),
onStorageError: (err, key) => logger.error('vanadium.storage_error', { err, key }),
},
});
// Pipe to Elasticsearch, Datadog, CloudWatch, etc.Observability
Integrate with error tracking and metrics:
const idempotency = createIdempotency({
adapter,
ttlMs: 86_400_000,
hooks: {
onTakeover: (ctx) => {
Sentry.captureEvent({ message: `crash recovery: ${ctx.key}`, extra: ctx });
},
onStorageError: (err, key) => {
Sentry.captureException(err, { extra: { key } });
},
},
});📝 License
MIT © Uday Thakur
🙏 Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details on:
- Code of conduct
- Development setup
- Pull request process
- Coding standards
- Architecture principles
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
🌟 Show Your Support
Give a ⭐️ if this project helped you build better applications!
Built with ❤️ by Uday Thakur for production-grade Node.js applications
