| name | local-cache-idempotency-fallback |
| description | Use database cache when external APIs aren't reliably idempotent. Use when:
(1) External API claims idempotency but returns different values for same input,
(2) Re-running a script creates duplicate resources, (3) Need stable identifiers
across runs but external service generates new ones. Pattern: check database cache
first, only call external API for genuinely new items, cache the result.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-01-25T00:00:00.000Z" |
Local Cache as Idempotency Fallback
Problem
External APIs that should be idempotent (same input = same output) sometimes aren't.
This causes problems when re-running scripts:
- Duplicate resources created
- Identifiers change between runs
- State becomes inconsistent across systems
Context / Trigger Conditions
- Re-running a script creates new resources instead of finding existing ones
- External API "fixed" idempotency but still returns different values
- Need stable identifiers (pubkeys, user IDs, resource IDs) across runs
- Script works once but fails on subsequent runs due to changed IDs
Solution
Use database cache as the source of truth for idempotency:
const { pubkey, token } = await externalApi.createUser(userId, username);
await db.saveUser({ userId, pubkey, token });
const cached = await db.getUser(userId);
let pubkey: string;
let token: string;
if (cached) {
pubkey = cached.pubkey;
token = cached.token;
console.log(`Using cached pubkey: ${pubkey}`);
} else {
const result = await externalApi.createUser(userId, username);
pubkey = result.pubkey;
token = result.token;
await db.saveUser({ userId, pubkey, token });
console.log(`Created new pubkey: ${pubkey}`);
}
Key Pattern
- Check local first: Always query your database before calling external API
- Use cached values: If found, use local values even if stale
- Only create when missing: External API called only for genuinely new items
- Cache immediately: Save result right after successful API call
- Log the source: Indicate whether value is "(cached)" or "(new)" for debugging
Verification
- Re-run script multiple times
- Same identifier used each time (from cache)
- No duplicate resources created in external system
- Script is idempotent regardless of external API behavior
Example
Real-world application - Keycast account creation:
const cached = await db.getImportedUser(creator.user_id);
let pubkey: string;
let token: string;
if (cached) {
pubkey = cached.pubkey;
token = cached.token;
console.log(`Pubkey: ${pubkey} (cached)`);
} else {
const result = await keycast.createPreloadedUser(
creator.user_id,
username,
displayName
);
pubkey = result.pubkey;
token = result.token;
console.log(`Pubkey: ${pubkey} (new)`);
await db.saveImportedUser({
vine_user_id: creator.user_id,
username: creator.username,
pubkey,
token,
});
}
Notes
- This pattern works even when the external API claims to be idempotent
- Database schema should use the input identifier as primary key (prevents duplicates)
- Consider adding timestamps to track when cached values were created
- For critical systems, add reconciliation logic to detect/fix drift
- The cache becomes your source of truth - treat it accordingly
Related Patterns
- Upsert on conflict: Use
ON CONFLICT DO UPDATE to handle race conditions
- Soft delete: Keep old records to track history of changes
- Cache invalidation: Add TTL or manual refresh if external values can legitimately change
Related Skills
stale-cache-external-service-recovery: What to do when external service loses data and
cached identifiers no longer exist (detect 404, delete cache, recreate)