---
title: Delivery identity
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/reactors/delivery-identity.mdx
description: A reactor can be handed the same event more than once, and nothing in the event itself says whether this is the first time. Declare a ReactorDelivery…
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

A reactor can be handed the same event more than once, and nothing in the event itself says whether this is the
first time. Declare a `ReactorDelivery` parameter on a handler method and Chronicle gives you a stable identity
for *this delivery of this event to this reactor* — the same value every time the event comes back, and a
different value for every genuinely different delivery.

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;

[EventType]
public record IdempotentPaymentDue(string OrderId, decimal Amount);

public interface IIdempotentPaymentGateway
{
    Task Charge(string orderId, decimal amount);
}

// Your storage, not Chronicle's. One row per completed delivery, keyed by its identity.
public interface IDeliveryReceipts
{
    Task<bool> HasCompleted(DeliveryId delivery);

    Task Complete(DeliveryId delivery);
}

public class IdempotentBilling(IIdempotentPaymentGateway payments, IDeliveryReceipts receipts) : IReactor
{
    public async Task PaymentDue(IdempotentPaymentDue @event, ReactorDelivery delivery)
    {
        if (await receipts.HasCompleted(delivery.Id))
        {
            return;
        }

        await payments.Charge(@event.OrderId, @event.Amount);
        await receipts.Complete(delivery.Id);
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/reactors/delivery-identity/basic.md)

</TabItem>
<TabItem label="Kotlin">

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/reactors/delivery-identity/basic.md)

</TabItem>
<TabItem label="Java">

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/reactors/delivery-identity/basic.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/reactors/delivery-identity/basic.md)

</TabItem>
<TabItem label="TypeScript">

Use an application-owned receipt store and payment gateway, supplied through your client artifact activator.

```typescript
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import { eventType, handles, reactor, EventContext, ReactorDelivery, ReactorServices } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';

@eventType()
class IdempotentPaymentDue {
    @field(String) readonly orderId: string;
    @field(Number) readonly amount: number;

    constructor(orderId: string = '', amount: number = 0) {
        this.orderId = orderId;
        this.amount = amount;
    }
}

interface IIdempotentPaymentGateway {
    charge(orderId: string, amount: number): Promise<void>;
}

// Your durable storage, not Chronicle's: one receipt per completed delivery.
interface IDeliveryReceipts {
    hasCompleted(deliveryId: string): Promise<boolean>;
    complete(deliveryId: string): Promise<void>;
}

@reactor('idempotent-billing')
class IdempotentBilling {
    constructor(private readonly payments: IIdempotentPaymentGateway, private readonly receipts: IDeliveryReceipts) {}

    @handles(IdempotentPaymentDue)
    async paymentDue(event: IdempotentPaymentDue, _context: EventContext,
        _services: ReactorServices, delivery: ReactorDelivery): Promise<void> {
        if (await this.receipts.hasCompleted(delivery.id)) return;

        await this.payments.charge(event.orderId, event.amount);
        // A crash before this write can repeat the charge. This is not exactly-once delivery.
        await this.receipts.complete(delivery.id);
    }
}
```

`delivery.id` joins reactor, event store, namespace, event sequence, partition and sequence number with `#`,
matching the .NET receipt key. Retry and replay use the same key. Keep the reactor id explicit and stable.
Chronicle still delivers at least once and stores no receipts. Make the receipt atomic with the effect where
possible; for a remote effect, use a gateway that accepts `delivery.id` as an idempotency key to close the gap.

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/reactors/delivery-identity/basic.md)

</TabItem>
</Tabs>

`delivery.Id` is the key. Record it when the side effect completes, check it before doing the side effect again.

## Why the existing markers are not enough

[OnceOnly](/chronicle/reactors/once-only/) and [Replay](/chronicle/reactors/replay/) both act on **replay** — the deliberate re-delivery you trigger by
rewinding an observer, or that a redaction or revision triggers for you. Neither one touches the other reason an
event comes back: your handler failed part-way through, the event-source partition paused, and recovering that
partition re-delivers the event as an ordinary observation. That is not a replay, so `OnceOnly` does not suppress
it — nor should it, since re-running the handler is the entire point of a retry. But if the handler had already
charged the card before it failed, the retry charges it again (see
[Troubleshooting](/chronicle/troubleshooting/) for the symptom, and
[Observers](/chronicle/hosting/configuration/observers/) for how retries and quarantine are configured).

The delivery identity is the seam for that case. It is the same across the failure and the recovery, so a record
you keep under it survives the retry.

```mermaid
sequenceDiagram
    participant C as Chronicle
    participant R as Reactor
    participant S as Your receipt store
    participant P as Payment API

    C->>R: PaymentDue (delivery A)
    R->>S: has A completed?
    S-->>R: no
    R->>P: charge
    R->>S: record A
    R--xC: handler fails after the charge
    Note over C: partition fails, then recovers
    C->>R: PaymentDue (delivery A — same identity)
    R->>S: has A completed?
    S-->>R: yes
    R-->>C: skip the charge, carry on
```

## How the three relate

| Mechanism | Covers replay | Covers recovery after a failure | Needs storage |
| --- | --- | --- | --- |
| `[OnceOnly]` | Yes — the handler is skipped | No | No |
| `[Replay]` | Yes — a different handler takes over | No | No |
| `ReactorDelivery` | Yes — a replay repeats the same delivery, so the identity matches | Yes | Yes — yours |

They compose rather than compete. Reach for `[OnceOnly]` or `[Replay]` first: they are declarative, cost nothing,
and cover replay completely. Add the delivery identity when the side effect must also survive a partition failure,
and keep the marker — `[OnceOnly]` then spares you a storage round-trip on every replayed event, and the receipt
covers the retries the marker cannot see.

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;

[EventType]
public record CustomerWelcomed(string CustomerId);

public interface IWelcomeMail
{
    Task Send(string customerId);
}

public class WelcomeMailer(IWelcomeMail mail, IDeliveryReceipts receipts) : IReactor
{
    [OnceOnly]
    public async Task SendWelcomeMail(CustomerWelcomed @event, ReactorDelivery delivery)
    {
        if (await receipts.HasCompleted(delivery.Id))
        {
            return;
        }

        await mail.Send(@event.CustomerId);
        await receipts.Complete(delivery.Id);
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/reactors/delivery-identity/with-once-only.md)

</TabItem>
<TabItem label="Kotlin">

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/reactors/delivery-identity/with-once-only.md)

</TabItem>
<TabItem label="Java">

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/reactors/delivery-identity/with-once-only.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/reactors/delivery-identity/with-once-only.md)

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/reactors/delivery-identity/with-once-only.md)

</TabItem>
</Tabs>

## What counts as the same delivery

The identity is built from six things Chronicle already knows, and nothing else:

| Component | What it is |
| --- | --- |
| `Reactor` | The reactor the event is delivered to |
| `EventStore` | The event store the event belongs to |
| `Namespace` | The namespace within that event store |
| `EventSequence` | The event sequence the reactor observes — the event log, or an inbox |
| `Partition` | The event source id the event is observed under |
| `SequenceNumber` | The event's position in the event sequence |

Two deliveries agreeing on all six are the same delivery; differing on any one of them makes them different. There
is deliberately nothing in there about *why* the event arrived, so a replay and the live delivery it repeats share
one identity. If a handler needs to know why, take an `EventContext` alongside and read its `ObservationState`.

`ReactorDelivery` is a record, so you can compare two of them directly. `delivery.Id` renders the same components
as a single string for use as a storage key.

:::caution[The identity is a persisted key]
Once you store receipts under `delivery.Id`, that string is part of your data. Renaming a reactor type without
giving it a stable id via `[Reactor("...")]`, moving a reactor to a different event sequence, or renaming a
namespace all change the identity — and every receipt written under the old one stops matching, so the side
effects they covered run again. Give any reactor whose deliveries you record an explicit id.
:::

## What it does and does not guarantee

It is an identity. It is not exactly-once delivery, and it does not make your reactor idempotent — it gives you the
one thing you cannot compute yourself, and leaves the rest to you.

- **Chronicle does not know whether your side effect ran.** It never sees your payment API or your mail server, so
  it cannot suppress a repeat on your behalf. Nothing is skipped unless your own code skips it.
- **Chronicle stores no receipt.** There is no framework-managed table of completed deliveries, no retention
  policy to configure, and no state added to your event store. The record is yours to write, index, and expire.
- **The gap between the effect and the record is still a gap.** Charge the card, die before writing the receipt,
  and the retry charges again. Recording the identity narrows at-least-once towards at-most-once exactly
  as far as your record is atomic with the effect — write both in one database transaction and the gap closes;
  call a remote API and it does not. Where you cannot close it, use the identity as the idempotency key the remote
  API itself accepts, and let it deduplicate.
- **Delivery is still at-least-once.** That is the contract Chronicle offers and this does not change it. The
  identity makes at-least-once *tractable*; it does not replace it.

If the integration you are calling is already idempotent, you do not need any of this — pass `delivery.Id` as its
idempotency key and stop there.

:::note[Client coverage]
`ReactorDelivery` is a .NET client feature. Elixir and TypeScript reactors have no equivalent parameter yet; make
those integrations self-idempotent instead.
:::
