# Conditional writes & idempotency

A conditional write gates a write on the object's current state, so two writers racing the same key cannot silently
clobber each other. An idempotency key makes a retried write safe to replay without writing twice.

This page covers both, where each lives across the clients, and the safe-retry patterns to reach for. The headline
distinction:

- **Conditional writes (`If-None-Match` / `If-Match`) are a native-client feature.** The native `putObject` takes them
  directly. The S3 `PutObject` does not, so use the native client or a conditional [copy](/guide/copying-objects) for an
  existing object.
- **Idempotency keys work on both clients.**

## Create-only: `If-None-Match: "*"`

`If-None-Match: "*"` writes only when the key is **absent**. If something is already there, the write fails with a `412`
precondition error and nothing is overwritten. This is the atomic "create, do not clobber" primitive: two callers racing
to create the same key, exactly one wins. It is a native-client write, so it always shows below the surface toggle.

::: code-group

```ts [Node]
import { isNativePreconditionFailed } from "@kelphect/sdk";

try {
  await native.putObject("reports", "q1/summary.txt", body, { ifNoneMatch: "*" });
  // we created it
} catch (err) {
  if (isNativePreconditionFailed(err)) {
    // someone else created it first
  } else throw err;
}
```

```go [Go]
_, err := native.PutObject(ctx, lockwellnative.PutObjectInput{
    Bucket: "reports", Key: "q1/summary.txt", Body: body,
    IfNoneMatch: "*",
})
if lockwellnative.IsPreconditionFailed(err) {
    // someone else created it first
} else if err != nil {
    return err
}
```

```java [Java]
import com.lockwell.sdk.nativeapi.NativeException;
import com.lockwell.sdk.nativeapi.NativeTypes.PutOptions;

try {
    nativeClient.putObject("reports", "q1/summary.txt", body,
        new PutOptions().ifAbsent()); // If-None-Match: *
} catch (NativeException e) {
    if (e.statusCode() == 412) {
        // someone else created it first
    } else throw e;
}
```

:::

## Overwrite-only: `If-Match: "<etag>"`

`If-Match: "<etag>"` writes only when the current object's ETag **matches** the value you pass. If the object changed
since you last read it (its ETag differs), the write fails with `412` and your update is rejected. This is optimistic
concurrency: read, compute a new value from what you read, then write back conditioned on the ETag you started from. A
mismatch means someone else wrote in between, so re-read and retry. This too is a native-client write.

::: code-group

```ts [Node]
// Read, mutate, write-back guarded by the ETag we read:
const cur = await native.getObject("config", "settings.json");
const next = patch(cur.body);

try {
  await native.putObject("config", "settings.json", next, { ifMatch: cur.etag });
} catch (err) {
  if (isNativePreconditionFailed(err)) {
    // lost the race; re-read and retry
  } else throw err;
}
```

```go [Go]
cur, err := native.GetObject(ctx, lockwellnative.GetObjectInput{Bucket: "config", Key: "settings.json"})
if err != nil {
    return err
}
etag := cur.ETag
body, _ := io.ReadAll(cur)
cur.Close()

_, err = native.PutObject(ctx, lockwellnative.PutObjectInput{
    Bucket: "config", Key: "settings.json", Body: bytes.NewReader(patch(body)),
    IfMatch: etag,
})
if lockwellnative.IsPreconditionFailed(err) {
    // lost the race; re-read and retry
}
```

```java [Java]
var cur = nativeClient.getObject("config", "settings.json");
byte[] body = cur.body().readAllBytes();
String etag = cur.etag();
cur.close();

nativeClient.putObject("config", "settings.json", patch(body),
    new PutOptions().ifMatch(etag));
// A 412 NativeException means the ETag moved; re-read and retry.
```

:::

The ETag is the object's content identity. `headObject` or any read returns it, and so does the `PutObject` result, so
you can chain writes without an extra round-trip.

## Conditional copy (the S3 path to conditional writes)

The S3 `PutObject` supports create-only `If-None-Match: *`; overwrite-only `If-Match` remains native-only. The S3
client also exposes copy-source conditionals. Native copy adds destination preconditions evaluated atomically at commit:

- `requireAbsent` copies only when the destination is absent (create-only).
- `requireMatchEtag` copies only when the destination's ETag matches (overwrite-only).

:::: s3

::: code-group

```ts [Node]
// Create-only via copy: never clobber an existing snapshot.
await native.copyObject("snapshots", "latest.json", {
  sourceBucket: "live",
  sourceKey: "state.json",
  requireAbsent: true, // 412 if snapshots/latest.json already exists
});
```

```go [Go]
_, err := native.CopyObject(ctx, lockwellnative.CopyObjectInput{
    Bucket: "snapshots", Key: "latest.json",
    SourceBucket: "live", SourceKey: "state.json",
    RequireAbsent: true, // 412 if snapshots/latest.json already exists
})
```

```java [Java]
nativeClient.copyObject("snapshots", "latest.json", "live", "state.json",
    new CopyOptions().ifAbsent()); // 412 if snapshots/latest.json already exists
```

:::

::::

Copy-**source** conditionals (gate on the source instead of the destination) are on both clients. See
[copying objects](/guide/copying-objects#copy-source-conditionals).

## Idempotency keys

An idempotency key makes a write **replay-safe**: a retry that carries the same key and the same payload returns the
original stored result instead of writing a second time. Reach for it when a network blip might make you re-send a
`PutObject` you are not sure landed.

It works on both clients, but the native client has one extra requirement.

### Native: pair the key with a body checksum

The native `putObject` streams the body and never buffers it, so it cannot hash the payload to prove a replay is the
same bytes. You supply that proof: pair the idempotency key with a `checksums` entry. The server verifies the digest
**before committing any bytes**, and uses the key plus the verified digest to collapse duplicate writes.

::: warning A native idempotent `putObject` needs a body checksum. Without a `checksums` entry, the streaming write has
no integrity proof and the server cannot confirm a retry is the same payload. :::

:::: native

::: code-group

```ts [Node]
import { sha256ChecksumBase64 } from "@kelphect/sdk";

const body = "invoice-payload";
await native.putObject("billing", "invoices/2026-001.json", body, {
  idempotencyKey: "invoice-2026-001",
  checksums: { sha256: await sha256ChecksumBase64(body) },
});
```

```go [Go]
import (
    "crypto/sha256"
    "encoding/base64"
)

sum := sha256.Sum256([]byte("invoice-payload"))
_, err := native.PutObject(ctx, lockwellnative.PutObjectInput{
    Bucket: "billing", Key: "invoices/2026-001.json",
    Body:           strings.NewReader("invoice-payload"),
    IdempotencyKey: "invoice-2026-001",
    Checksums:      map[string]string{"sha256": base64.StdEncoding.EncodeToString(sum[:])},
})
```

```java [Java]
import java.security.MessageDigest;
import java.util.Base64;

byte[] body = "invoice-payload".getBytes();
String sha256 = Base64.getEncoder().encodeToString(
    MessageDigest.getInstance("SHA-256").digest(body));

nativeClient.putObject("billing", "invoices/2026-001.json", body,
    new PutOptions()
        .idempotencyKey("invoice-2026-001")
        .checksum("sha256", sha256));
```

:::

::::

Supported algorithms include `sha256`, `sha1`, `crc32`, and `crc32c`. See [checksums](/guide/checksums) for the full
set.

### S3: the buffered PutObject

The S3 `PutObject` buffers the body and signs its exact SHA-256, so an idempotency key needs no extra checksum: the
signed body hash is the integrity proof. Pass `WithIdempotencyKey` / `idempotencyKey` and the SDK signs the header so it
cannot be stripped or altered in transit.

:::: s3

::: code-group

```ts [Node]
await s3.putObject("billing", "invoices/2026-001.json", Buffer.from(body), {
  idempotencyKey: "invoice-2026-001",
});
```

```go [Go]
_, err := s3.PutObject(ctx, "billing", "invoices/2026-001.json", body,
    lockwellsdk.WithIdempotencyKey("invoice-2026-001"),
)
```

```java [Java]
s3.putObject("billing", "invoices/2026-001.json", body,
    new LockwellClient.PutOptions().idempotencyKey("invoice-2026-001"));
```

:::

::::

The S3 streaming upload (`putObjectStream`) does **not** accept an idempotency key: its trailing checksum is not known
when the write is reserved. Use the buffered `PutObject` when you need idempotency, or
[multipart upload](/guide/multipart-uploads) for a large object (complete-multipart accepts an idempotency key).

## How idempotency interacts with retries

Every native client refreshes its token before expiry and re-mints once on a `401`. The Java native client also retries
transient failures by default, but it only replays `PUT` or `POST` when the request carries an idempotency key. Carry an
idempotency key (with its checksum) so any client-side retry is collapsed server-side. Retry tuning is on
[Errors & retries](/guide/errors-and-retries).

The S3 client's automatic retry policy is idempotency-aware. It always retries safe methods (GET, HEAD, DELETE), and it
retries a `PutObject` / `POST` **only** when the request carries an idempotency key and has a re-readable buffered body.
A streaming body is never auto-retried (the reader is already consumed). This is exactly why an idempotency key matters:
it is what lets the client safely replay a write after a transport error or a `5xx`.

:::: s3

```go
// Default policy: up to 3 attempts with backoff + jitter. A keyed PutObject is
// eligible for automatic retry; without the key it is attempted once.
s3, _ := lockwellsdk.New(endpoint, creds,
    lockwellsdk.WithRetryPolicy(lockwellsdk.DefaultRetryPolicy()))
```

::::

## Choosing the right guard

| You want to...                                | Use                                                                            |
| --------------------------------------------- | ------------------------------------------------------------------------------ |
| Create a key only if it does not exist        | native `putObject` with `ifNoneMatch: "*"`, or a copy with `requireAbsent`     |
| Overwrite only if unchanged since you read it | native `putObject` with `ifMatch: "<etag>"`, or a copy with `requireMatchEtag` |
| Make a retried write replay safely            | an idempotency key (native: pair with a checksum)                              |
| Copy only if the source still matches         | copy-source `ifMatch` / `ifNoneMatch` (both clients)                           |

## Next steps

- [Upload & download](/guide/data-operations): the write these conditions gate.
- [Copying objects](/guide/copying-objects): destination and source conditionals on copy.
- [Checksums](/guide/checksums): the body-integrity digests native idempotency needs.
- [Errors & retries](/guide/errors-and-retries): the retry policy that replays keyed writes.
