This implementation adds database-level constraints to prevent duplicate donations, ensuring idempotency is enforced structurally rather than just logically.
Added to transactions table:
idempotencyKey TEXT UNIQUE- Ensures no duplicate donations can be persisted- Index on
idempotencyKeyfor fast lookups
Migration:
CREATE TABLE transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
senderId INTEGER NOT NULL,
receiverId INTEGER NOT NULL,
amount REAL NOT NULL,
memo TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
idempotencyKey TEXT UNIQUE, -- NEW: Enforces uniqueness at DB level
FOREIGN KEY (senderId) REFERENCES users(id),
FOREIGN KEY (receiverId) REFERENCES users(id)
);
CREATE INDEX idx_transactions_idempotency ON transactions(idempotencyKey);New Error Class:
class DuplicateError extends AppError {
constructor(message = 'Duplicate entry detected', code = ERROR_CODES.DUPLICATE_DONATION) {
super(code, message, 409);
}
}Database Utility Enhancement:
static isUniqueConstraintError(err) {
return err && err.code === 'SQLITE_CONSTRAINT' && err.message.includes('UNIQUE');
}All database operations now detect UNIQUE constraint violations and throw DuplicateError with HTTP 409 status.
Success (201 Created):
{
"success": true,
"data": {
"id": 123,
"stellarTxId": "abc123...",
"amount": 50.0,
"timestamp": "2024-02-25T10:00:00.000Z"
}
}Duplicate Detected (409 Conflict):
{
"success": false,
"error": {
"code": "DUPLICATE_DONATION",
"message": "Duplicate donation detected - this transaction has already been processed",
"timestamp": "2024-02-25T10:00:01.000Z"
}
}- Client provides
Idempotency-Keyheader (UUID recommended) - Key is validated and stored with the transaction
- Same key = same transaction (idempotent)
Layer 1: Application Logic (Existing)
- Middleware checks for existing idempotency key
- Returns cached response if found
- Prevents duplicate processing
Layer 2: Database Constraint (NEW)
- UNIQUE constraint on
idempotencyKeycolumn - Prevents duplicate persistence even if logic fails
- Last line of defense against race conditions
Scenario: Two identical requests arrive simultaneously
Request A ──┐
├──> Middleware (both pass) ──┐
Request B ──┘ │
▼
Database INSERT
│
┌─────────────────┴─────────────────┐
▼ ▼
Request A: SUCCESS Request B: DUPLICATE
(201 Created) (409 Conflict)
The database UNIQUE constraint ensures only one succeeds.
- ✅
src/scripts/initDB.js- Added idempotencyKey column with UNIQUE constraint - ✅
src/scripts/migrations/001_add_idempotency_constraint.js- Migration for existing databases
- ✅
src/utils/errors.js- AddedDuplicateErrorclass andDUPLICATE_DONATIONcode - ✅
src/utils/database.js- Added UNIQUE constraint detection and error mapping
- ✅
src/routes/donation.js- Added idempotencyKey to INSERT, added conflict handling
- ✅
src/middleware/errorHandler.js- Already handles AppError (includes DuplicateError)
1. Create a donation:
curl -X POST http://localhost:3000/donations/send \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-key-123" \
-H "X-API-Key: your-api-key" \
-d '{
"senderId": 1,
"receiverId": 2,
"amount": 50.0,
"memo": "Test donation"
}'Expected: 201 Created
2. Retry with same key:
# Same request, same idempotency key
curl -X POST http://localhost:3000/donations/send \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-key-123" \
-H "X-API-Key: your-api-key" \
-d '{
"senderId": 1,
"receiverId": 2,
"amount": 50.0,
"memo": "Test donation"
}'Expected: 409 Conflict with DUPLICATE_DONATION error
Run existing test suite:
npm testAll idempotency tests should pass with the new database-level enforcement.
- Run
npm run init-db- Schema includes idempotency constraint
- Run migration:
node src/scripts/migrations/001_add_idempotency_constraint.js
- Verify:
sqlite3 data/stellar_donations.db "PRAGMA table_info(transactions);" - Confirm
idempotencyKeycolumn exists with UNIQUE constraint
- ✅ Impossible to persist duplicate donations
- ✅ Database enforces uniqueness regardless of application logic
- ✅ Protection against race conditions
- ✅ Multiple layers of protection (middleware + database)
- ✅ Graceful error handling with clear messages
- ✅ Consistent behavior across all donation endpoints
- ✅ Index on idempotencyKey for fast lookups
- ✅ Database-level check is faster than application logic
- ✅ No additional network calls
- ✅ No breaking changes to API
- ✅ Backward compatible (idempotencyKey is optional for old records)
- ✅ Clear error responses guide clients
✅ Duplicate donations cannot be persisted
- UNIQUE constraint on idempotencyKey prevents duplicates at database level
- Tested with concurrent requests
✅ API responds safely on conflict
- Returns 409 Conflict with clear error message
- Includes error code DUPLICATE_DONATION
- No data corruption or crashes
✅ Idempotency key strategy designed
- Client-provided UUID in Idempotency-Key header
- Stored with transaction
- Enforced at both application and database levels
✅ Unique constraints added
- idempotencyKey column with UNIQUE constraint
- Index for performance
- Migration script for existing databases
✅ Conflict errors handled gracefully
- DuplicateError class for type-safe handling
- Automatic detection in Database utility
- Consistent error responses across all endpoints
- Idempotency keys are not sensitive data (UUIDs)
- No PII stored in idempotency keys
- Keys expire with idempotency cache (24 hours default)
- Database constraint prevents timing attacks on duplicate detection
- Minimal overhead: Index lookup is O(log n)
- Faster duplicate detection: Database check vs application logic
- No additional queries: Constraint checked during INSERT
- Improved throughput: Prevents unnecessary processing of duplicates