Tools: SwiftUI Idempotency & Duplicate Prevention (Correctness in Distributed Systems)

Tools: SwiftUI Idempotency & Duplicate Prevention (Correctness in Distributed Systems)

Source: Dev.to

🧠 The Core Principle ## 🧱 1. What Is Idempotency? ## ⚠️ 2. Why Mobile Apps Need Idempotency ## 🧬 3. Attach an Operation ID to Every Mutation ## 🌐 4. Server-Side Idempotency Keys ## πŸ“¦ 5. Local Deduplication Layer ## πŸ” 6. Idempotency in Sync Queues ## 🧱 7. Designing Idempotent APIs ## πŸ”„ 8. Handling Partial Failures ## ⚠️ 9. Common Anti-Patterns ## πŸ§ͺ 10. Testing Idempotency ## πŸš€ Final Thoughts Most apps assume actions happen once: 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: Retrying an operation must not change the result. If running an action twice creates a different outcome, your system is fragile. An operation is idempotent if running it multiple times has the same effect as running it once. Running twice β†’ item is still deleted. Running twice β†’ two orders created. Mobile environments guarantee retries: Without idempotency, you get: Every mutation must have a stable identity. The ID travels through: Send the operation ID with requests: This guarantees safe retries. Prevent duplicate execution locally. Persist processed IDs for crash safety. Without it, retries corrupt data. Prefer operations that include identity. PUT is idempotent by design. Network failure after server success is common. Your system becomes self-healing. If your system survives these, it’s robust. β€œThis request will only run once.” Idempotency gives you: This is the difference between: Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: submitOrder() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: submitOrder() CODE_BLOCK: submitOrder() CODE_BLOCK: deleteItem(id) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: deleteItem(id) CODE_BLOCK: deleteItem(id) CODE_BLOCK: createOrder() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: createOrder() CODE_BLOCK: createOrder() CODE_BLOCK: struct OperationID: Hashable, Codable { let value: UUID } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: struct OperationID: Hashable, Codable { let value: UUID } CODE_BLOCK: struct OperationID: Hashable, Codable { let value: UUID } CODE_BLOCK: struct CreateOrderOperation { let id: OperationID let payload: OrderPayload } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: struct CreateOrderOperation { let id: OperationID let payload: OrderPayload } CODE_BLOCK: struct CreateOrderOperation { let id: OperationID let payload: OrderPayload } CODE_BLOCK: POST /orders Idempotency-Key: 8F6C-1234-ABCD Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: POST /orders Idempotency-Key: 8F6C-1234-ABCD CODE_BLOCK: POST /orders Idempotency-Key: 8F6C-1234-ABCD COMMAND_BLOCK: final class OperationDeduplicator { private var processed: Set<OperationID> = [] func shouldProcess(_ id: OperationID) -> Bool { processed.insert(id).inserted } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: final class OperationDeduplicator { private var processed: Set<OperationID> = [] func shouldProcess(_ id: OperationID) -> Bool { processed.insert(id).inserted } } COMMAND_BLOCK: final class OperationDeduplicator { private var processed: Set<OperationID> = [] func shouldProcess(_ id: OperationID) -> Bool { processed.insert(id).inserted } } CODE_BLOCK: guard deduplicator.shouldProcess(op.id) else { return } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: guard deduplicator.shouldProcess(op.id) else { return } CODE_BLOCK: guard deduplicator.shouldProcess(op.id) else { return } CODE_BLOCK: POST /cart/items Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: POST /cart/items CODE_BLOCK: POST /cart/items CODE_BLOCK: PUT /cart/items/{itemID} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: PUT /cart/items/{itemID} CODE_BLOCK: PUT /cart/items/{itemID} CODE_BLOCK: User Action β†’ Operation ID β†’ Persistent Queue β†’ Retry β†’ Server Deduplication β†’ Consistent Result Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: User Action β†’ Operation ID β†’ Persistent Queue β†’ Retry β†’ Server Deduplication β†’ Consistent Result CODE_BLOCK: User Action β†’ Operation ID β†’ Persistent Queue β†’ Retry β†’ Server Deduplication β†’ Consistent Result - retries after network failure - background sync - offline queues - app relaunch recovery - migration reprocessing - slow server responses - user double taps - safe to retry - duplicate-proof - crash-resilient - sync-friendly - production-grade - network timeouts trigger retries - background tasks may run twice - app relaunch replays queued operations - sync engines retry failed operations - users double tap buttons - duplicate charges - duplicated messages - corrupted state - inconsistent sync - local queue - network request - server processing - reconciliation - if key is new β†’ process - if key exists β†’ return previous result - retries are safe - crash recovery is safe - migrations can replay operations - background sync won’t duplicate - retry creates duplicate - retry returns existing result - relying on timestamps for uniqueness - generating IDs on retry - storing processed IDs only in memory - assuming requests run once - not propagating IDs to server - duplicate charges - duplicated records - unrecoverable inconsistencies - retry same operation 10Γ— - crash before response received - replay queue after migration - simulate slow server responses - double-tap user actions - safe retries - reliable sync - crash resilience - duplicate prevention - consistent distributed systems - a fragile mobile client - and a production-grade system