Reliability

Idempotency keys, the five delivery states, the exception taxonomy, async cancellation, and bounded admission in the Repost Java client.

Every send settles in a known state, even under timeouts, cancellation, or overload. This page covers the outcome model end to end: how idempotency makes retries safe, what each delivery state means, and how cancellation and backpressure behave.

Idempotency and delivery outcomes

Repost dedupes on the idempotency key. When you don't pass one, the runtime generates a stable key per operation and reuses it across that operation's retries. When you pass one via SendOptions, reuse the same key to make a send safe to repeat.

Every failure is a RepostException carrying a stable RepostErrorCode, a best-known DeliveryState, the idempotencyKey, the started attemptCount, and an isRetryable() flag, never a raw server body, header, or nested throwable. The five delivery states:

DeliveryStateMeaningWhat to do
ACCEPTEDRepost accepted the event.Done: you get a SendResult, not an exception.
NOT_SENTThe request definitely never left.Safe to retry with the same key.
POSSIBLY_SENTThe request may have been received; the response was lost, ambiguous (HTTP 409), or the deadline elapsed mid-flight.Reuse the same idempotency key and reconcile, never mint a new key, or you risk a duplicate.
REJECTEDRepost definitively rejected it (a non-retryable 4xx).Fix the request; do not retry blindly.
CANCELLED_UNKNOWNYou cancelled after the request may have been sent.Same as POSSIBLY_SENT: reconcile with the same key.
try {
    SendResult result = repost.webhooks().order().created("customer-123", order);
    System.out.println("accepted " + result.getId());
} catch (RepostException failure) {
    if (failure.getDeliveryState() == DeliveryState.POSSIBLY_SENT) {
        // Repost may already have the event. Reuse the SAME idempotency key and
        // reconcile — never mint a new key, or you risk a duplicate delivery.
        reconcile(failure.getIdempotencyKey());
    } else if (failure.isRetryable()) {
        // Definitely not sent and safe to retry with the same idempotency key.
        retryLater(order, failure.getIdempotencyKey());
    } else {
        // Terminal failure. Inspect the low-cardinality code; the message and
        // exception carry no payload or credentials.
        RepostErrorCode code = failure.getErrorCode();
        throw new IllegalStateException("send failed: " + code, null);
    }
}

The exception subclass tells you the category without a switch: RepostConfigurationException (bad config), RepostValidationException (your model failed validation, NOT_SENT), RepostSerializationException, RepostTransportException (connect/TLS/timeout/cancel/overload), RepostPublishException (the server returned a rejection or a retryable failure), and RepostDescriptorVersionException (regenerate or upgrade: the client and runtime versions are incompatible).

Async and cancellation

createdAsync(...) returns a SendOperation, which is both a CompletionStage<SendResult> and a Future<SendResult>. Cancel it like any Future; the delivery outcome still settles on a separate, non-cancellable stage you read with outcome():

SendOperation operation = repost.webhooks().order().createdAsync("customer-123", order);
 
// Cancellation is cooperative: cancel on shutdown or when the caller gives up.
operation.cancel(true);
 
// The result stage may finish exceptionally, but the delivery outcome always settles
// and never throws — read it to reconcile.
operation.outcome().thenAccept(outcome -> {
    if (outcome.getDeliveryState() == DeliveryState.CANCELLED_UNKNOWN) {
        // The request may have reached Repost. Reconcile with the same key.
        reconcile(outcome.getIdempotencyKey());
    }
});

Cancelling before the request commits settles NOT_SENT; cancelling after it may have been written settles CANCELLED_UNKNOWN. Either way the operation settles exactly once and closes only the work it owns.

Bounded admission

maxInFlightOperations is a hard bulkhead, not a queue. When more operations are in flight than the budget allows, a new send is rejected immediately with an OVERLOADED error and NOT_SENT: the runtime never buffers unbounded work or paces your traffic. Treat it as backpressure:

ClientOptions options = ClientOptions.builder()
        .apiKey(System.getenv("REPOST_SEND_API_KEY"))
        .maxInFlightOperations(64)
        .build();
try (RepostClient repost = RepostClient.create(options)) {
    try {
        repost.webhooks().order().created("customer-123", order);
    } catch (RepostException failure) {
        if (failure.getErrorCode() == RepostErrorCode.OVERLOADED) {
            // The bounded in-flight budget is full. The event was NOT sent; apply your
            // own backpressure and retry — the runtime never queues past the budget.
            retryLater(order, failure.getIdempotencyKey());
        }
    }
}

Continue