Approach (brief): Use a small, O(1) lookup store per (tenant_id, idempotency_key) with TTL. Use Redis (cluster or managed) storing a marker or the full response atomically with SET key NX EX — this deduplicates retries, is low-latency and supports expiry. For 100k tenants the keyspace is large but Redis handles millions of keys; to reduce memory store only minimal metadata (status/pointer) and keep full payloads in object store if needed.Pseudocode:python
# python pseudocode
from flask import Flask, request, jsonify
import redis
import json
r = redis.Redis(host='redis', port=6379, decode_responses=True) # use cluster in prod
app = Flask(__name__)
IDEMP_TTL = 24 * 3600 # TTL for idempotency keys
def make_key(tenant_id, idem_key):
return f"tenant:{tenant_id}:idem:{idem_key}"
@app.route("/ingest", methods=["POST"])
def ingest():
payload = request.json
tenant = payload['tenant_id']
idem = payload['idempotency_key']
key = make_key(tenant, idem)
# Try to create marker atomically; if it exists, return stored result/status
# Use SET NX EX to ensure single writer wins and key expires automatically
created = r.set(key, json.dumps({"status": "processing"}), nx=True, ex=IDEMP_TTL)
if not created:
# Key exists: fetch status/result and return dedup response
stored = r.get(key)
return jsonify({"dedup": True, "stored": json.loads(stored)}), 200
# We won the right to process
try:
result = process_event(payload) # idempotent processing logic / enqueue to pipeline
# store final result (or pointer) with same TTL
r.set(key, json.dumps({"status": "done", "result_ptr": result.get("ptr")}), ex=IDEMP_TTL)
return jsonify(result), 201
except Exception as e:
# on failure set a short TTL so retries can attempt again
r.set(key, json.dumps({"status": "failed", "error": str(e)}), ex=60)
raise
Key concepts:- Atomic SET NX EX avoids race conditions.- Store minimal info (status + pointer) to keep memory low.- TTL enables automatic expiry of old keys.Persistence & scaling choices:- Redis cluster (sharded) with eviction policy and memory monitoring.- For very tight memory: use Redis Bloom? (not appropriate for exact dedupe). Better: store only hashes / pointers; large responses go to S3 and reference via pointer.- Optionally log idempotency records to durable DB (Postgres, Cassandra) asynchronously if long-term audit required.Complexity:- Time: O(1) per request (Redis ops)- Space: O(number of active unique (tenant, idem) keys). With TTL this is bounded by throughput * TTL.Edge cases:- Clock/TTL misconfigurations — ensure TTL long enough for processing.- Redis failover — use cluster + replicas; consider fallback to durable DB.- Very large number of unique keys — monitor memory, increase TTL only as needed, or use rolling compaction to move old entries to cheaper storage.