# Deduplicate effects

Choose the key for one logical operation and keep it stable across retries. A fresh random key on every attempt creates a new operation.

| Boundary | Mechanism | What to do |
| --- | --- | --- |
| Authenticated HTTP write | Automatic durable idempotency guard | Reuse the same `Idempotency-Key`; understand status replay in the [HTTP guide](<https://sandloom.dev/docs/modules/http/idempotent-write/index.md>) |
| Event consumer | `@on(..., durable=True)` (the default) | Let the consumer's unit claim the envelope ID; duplicate delivery must not repeat committed effects |
| Contract command | `@command(idempotency_key=..., idempotency_scope=...)` | Declare the operation-key argument and the account scope; callers reuse both |
| Task or direct command effect | A durable inbox claim or a unique operation key in the effect's transaction | Atomically claim and write; task redelivery alone is not deduplication |
| External payment or notification | Provider idempotency key plus durable reconciliation | Reuse the provider key after timeouts; resolve ambiguous outcomes before compensating |

The SDK exposes `durable_inbox` and `operation_key` through [`sandloom.sdk.effects`](<https://sandloom.dev/docs/reference/sdk/effects/index.md>), and the command declaration through [`sandloom.sdk.contracts`](<https://sandloom.dev/docs/reference/sdk/contracts/index.md>). These are advanced tools: choose a scope that cannot collide between unrelated tenants, accounts or operation types.

## Keep the claim and the effect together

A consumer's database claim and its database writes commit in the same unit. A rollback leaves the delivery available for retry. A claim committed before an unrelated effect can lose work; an effect committed before its claim can repeat work. Do not add a second dedupe store alongside the framework's model.

For a task, write a regression that delivers the same logical operation twice and checks the stored balance, ledger row or other final effect. For consumers, use `deliver(event, times=2)` with the same envelope identity, and also check the rollback-and-retry path. The [testing reference](<https://sandloom.dev/docs/reference/testing/index.md>) explains the real consumer runtime and helpers.

## Boundaries and retention

At-least-once delivery does not become global exactly-once execution. Different envelope IDs are different deliveries. Outbox retention, inbox retention and command-log TTL must cover the retry horizon; startup checks refuse inconsistent windows. An external provider does not participate in the database transaction. Use an [operation intent](<https://sandloom.dev/docs/modules/http/route-policy/index.md>) or a [saga](<https://sandloom.dev/docs/modules/between/sagas/index.md>) when the process crosses those boundaries.
