Most apps assume actions happen once:
submitOrder()
That works…
until you introduce:
- retries after network failure
- background sync
- offline queues
- app relaunch recovery
- migration reprocessing
- slow server responses
- user double taps
At that point, duplicate operations become one of the most dangerous bugs in your app.
This post shows how to design idempotent systems in SwiftUI that are:
- safe to retry
- duplicate-proof
- crash-resilient
- sync-friendly
- production-grade
🧠 The Core Principle
Retrying an operation must not change the result.
If running an action twice creates a different outcome, your system is fragile.
🧱 1. What Is Idempotency?
An operation is idempotent if running it multiple times has the same effect as running it once.
Safe:
deleteItem(id)
Running twice → item is still deleted.
Unsafe:
createOrder()
Running twice → two orders created.
⚠️ 2. Why Mobile Apps Need Idempotency
Mobile environments guarantee retries:
- network timeouts trigger retries
- background tasks may run twice
- app relaunch replays queued operations
- sync engines retry failed operations
- users double tap buttons
Without idempotency, you get:
- duplicate charges
- duplicated messages
- corrupted state
- inconsistent sync
🧬 3. Attach an Operation ID to Every Mutation
Every mutation must have a stable identity.
struct OperationID: Hashable, Codable {
let value: UUID
}
Example operation:
struct CreateOrderOperation {
let id: OperationID
let payload: OrderPayload
}
The ID travels through:
- local queue
- network request
- server processing
- reconciliation
🌐 4. Server-Side Idempotency Keys
Send the operation ID with requests:
POST /orders
Idempotency-Key: 8F6C-1234-ABCD
Server logic:
- if key is new → process
- if key exists → return previous result
This guarantees safe retries.
📦 5. Local Deduplication Layer
Prevent duplicate execution locally.
final class OperationDeduplicator {
private var processed: Set<OperationID> = []
func shouldProcess(_ id: OperationID) -> Bool {
processed.insert(id).inserted
}
}
Usage:
guard deduplicator.shouldProcess(op.id) else { return }
Persist processed IDs for crash safety.
🔁 6. Idempotency in Sync Queues
With idempotency:
- retries are safe
- crash recovery is safe
- migrations can replay operations
- background sync won’t duplicate
Without it, retries corrupt data.
🧱 7. Designing Idempotent APIs
Prefer operations that include identity.
Bad:
POST /cart/items
Better:
PUT /cart/items/{itemID}
PUT is idempotent by design.
🔄 8. Handling Partial Failures
Network failure after server success is common.
Without idempotency:
- retry creates duplicate
With idempotency:
- retry returns existing result
Your system becomes self-healing.
⚠️ 9. Common Anti-Patterns
Avoid:
- relying on timestamps for uniqueness
- generating IDs on retry
- storing processed IDs only in memory
- assuming requests run once
- not propagating IDs to server
These lead to:
- duplicate charges
- duplicated records
- unrecoverable inconsistencies
🧪 10. Testing Idempotency
Test scenarios:
- retry same operation 10×
- crash before response received
- replay queue after migration
- simulate slow server responses
- double-tap user actions
If your system survives these, it’s robust.
🧠 Mental Model
Think:
User Action
→ Operation ID
→ Persistent Queue
→ Retry
→ Server Deduplication
→ Consistent Result
Not:
“This request will only run once.”
🚀 Final Thoughts
Idempotency gives you:
- safe retries
- reliable sync
- crash resilience
- duplicate prevention
- consistent distributed systems
This is the difference between:
- a fragile mobile client
- and a production-grade system