Skip to main content

resonate-external-system-of-record-pattern-java

Maintain consistency between a Resonate Java workflow and an external system that owns the truth — a database, ledger, or third-party API — without distributed transactions, by wrapping every interaction in its own idempotent ctx.run step so the durable promise records the result and replay never re-fires the external call. Uses type-keyed dependency injection (r.withDependency / ctx.getDependency) to reach the SoR client, and idempotency keys derived from ctx.info().id(). Verified against develop/java.mdx (docs PR

Ir para a instalação

Informações da origem

Repositório
resonatehq/resonate-skills
Última atividade na origem
30 de junho de 2026 às 17:35
Idioma detectado do SKILL.md
inglês
Estrelas
6
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
resonate-external-system-of-record-pattern-java
description
Maintain consistency between a Resonate Java workflow and an external system that owns the truth — a database, ledger, or third-party API — without distributed transactions, by wrapping every interaction in its own idempotent ctx.run step so the durable promise records the result and replay never re-fires the external call. Uses type-keyed dependency injection (r.withDependency / ctx.getDependency) to reach the SoR client, and idempotency keys derived from ctx.info().id(). Verified against develop/java.mdx (docs PR
license
Apache-2.0
# Resonate External System of Record Pattern — Java > **Prerelease note.** `resonate-sdk-java` is published on Maven Central — pin `io.resonatehq:resonate-sdk-java:0.1.1`. The API mirrors the Python SDK and may change before a stable `1.0`. **Requires Java 21+** (virtual threads, a feature generally available in Java 21). There is no Java system-of-record example repo yet, so every code block here uses only documented SDK surface, compile-verified against `0.1.1` and cross-checked against `develop/java.mdx` (docs PR #230) and the SDK source. ## Overview For the language-agnostic framing and the "Write Last, Read First" mental model, see `resonate-external-system-of-record-pattern-typescript`. This skill translates that model to idiomatic Java: `ctx.run`-based checkpointing, type-keyed dependency injection for the external client, and idempotency keys derived from the durable execution's stable ID. The core contract is simple: one system owns the truth. Resonate coordinates the workflow and guarantees at-least-once execution; the external system enforces consistency through its own primitives — idempotency keys, upserts, conditional writes, ledger deduplication. ## When to use - A Resonate workflow must create, update, or read from a database, payment processor, ledger, or any external store that has its own durability. - You need multi-step operations (reserve → charge → record) to be safe to retry without double-writes. - You cannot use distributed transactions across Resonate's promise store and the external system. Do not use this pattern when all writes live inside a single ACID transaction scope — use the transaction directly. ## The replay principle — wrap every SoR call in `ctx.run` Whenever a workflow suspends and resumes (after a `ctx.sleep`, a remote `ctx.rpc`, or a pending `ctx.promise`), the entire workflow body re-executes from the top. Durable child promises short-circuit steps that already settled — the external call is not re-fired, and the stored result is returned directly. But any external call NOT wrapped in a `ctx.run` will execute again on every replay, producing double-writes. Rule: **every read from or write to the external SoR must be inside its own `ctx.run` leaf.** ```java // BAD — fires on every replay, may charge the card twice public static String placeOrder(Context ctx, OrderArgs args) { String chargeId = stripe.charge(args.amountCents(), args.cardToken()); // replay fires this again return chargeId; } // GOOD — checkpointed; on replay the stored chargeId is returned, Stripe is not called again public static String placeOrder(Context ctx, OrderArgs args) { return ctx.run(Orders::chargeCard, args).await(); } ``` ## Reaching the SoR client via dependency injection The Java SDK uses type-keyed DI. Register the client once on the instance with `r.withDependency`, and fetch it inside a leaf by type with `ctx.getDependency`. (Go takes a different path — it closes the client over the leaf function.) ```java import io.resonatehq.resonate.Context; // In the ephemeral world, before processing starts: // r.withDependency(new PaymentGateway(System.getenv("STRIPE_KEY"))); public static String chargeCard(Context ctx, ChargeArgs args) { PaymentGateway gateway = ctx.getDependency(PaymentGateway.class); // Idempotency key derived from a STABLE value — never time/random. String idempotencyKey = "charge:" + args.orderId(); return gateway.submit(args.amountCents(), args.cardToken(), idempotencyKey); } ``` Dependencies are keyed by their concrete class — register at most one instance per type. This keeps the SoR client out of the serialized args while still being reachable from any worker that runs the leaf. ## Idempotency keys from `ctx.info().id()` A `ctx.run` step can re-execute on retry before it settles (Resonate delivers at-least-once). The SoR must be addressed idempotently so a duplicate call is harmless. Derive stable keys from `ctx.info().id()`, `ctx.info().originId()`, or stable workflow args — never from `System.currentTimeMillis()` or `Math.random()`, which differ across replays and defeat deduplication. ```java public static long applyBalanceDelta(Context ctx, AccountArgs args) { Ledger ledger = ctx.getDependency(Ledger.class); String idempotencyKey = args.accountId() + ":" + ctx.info().id() + ":adjust"; return ledger.apply(args.accountId(), args.delta(), idempotencyKey); } ``` `ctx.info().id()` is stable across replays, so the derived key is identical on every retry — the SoR deduplicates. ## Full order workflow: reserve → charge → record Three sequential SoR interactions, each in its own `ctx.run`. Any step that already settled short-circuits on replay; no external system is hit twice. ```java import io.resonatehq.resonate.Context; public final class Orders { private Orders() {} public record OrderArgs(String orderId, String sku, int quantity, long amountCents, String cardToken) {} public record ReserveArgs(String orderId, String sku, int quantity) {} public record ChargeArgs(String orderId, long amountCents, String cardToken) {} public record RecordArgs(String orderId, String chargeId) {} /** Orchestrates three idempotent SoR steps. Each ctx.run is checkpointed. */ public static String createOrder(Context ctx, OrderArgs args) { // Step 1 — reserve inventory; idempotent via ON CONFLICT in the DB String reservationId = ctx.run(Orders::reserveInventory, new ReserveArgs(args.orderId(), args.sku(), args.quantity())).await(); // Step 2 — charge payment; idempotent via the gateway's idempotency key String chargeId = ctx.run(Orders::chargeCard, new ChargeArgs(args.orderId(), args.amountCents(), args.cardToken())).await(); // Step 3 — record the order; idempotent via ON CONFLICT (order_id) DO NOTHING ctx.run(Orders::recordOrder, new RecordArgs(args.orderId(), chargeId)).await(); return args.orderId(); } public static String reserveInventory(Context ctx, ReserveArgs args) { Inventory inv = ctx.getDependency(Inventory.class); return inv.reserve(args.orderId(), args.sku(), args.quantity()); // ON CONFLICT → existing id } public static String chargeCard(Context ctx, ChargeArgs args) { PaymentGateway gateway = ctx.getDependency(PaymentGateway.class); return gateway.submit(args.amountCents(), args.cardToken(), "charge:" + args.orderId()); } public static String recordOrder(Context ctx, RecordArgs args) { OrderStore store = ctx.getDependency(OrderStore.class); store.insertIfAbsent(args.orderId(), args.chargeId()); // ON CONFLICT (order_id) DO NOTHING return "recorded"; } } ``` ## Check-then-act made replay-safe A common SoR pattern is: read current state, branch on it, then apply a mutation. Both the read and the write must be in separate `ctx.run` calls. Reading the SoR in bare workflow code is unsafe — on replay the bare read fires again and may observe a different value. ```java public static long adjustBalance(Context ctx, AccountArgs args) { // Checkpointed read — on replay returns the stored snapshot, not a fresh DB hit. AccountState state = ctx.run(Orders::getAccountState, args.accountId()).await(); // Branch in plain workflow code — no external calls here. if (state.status().equals("frozen")) { throw new IllegalStateException("account " + args.accountId() + " is frozen"); } if (state.balance() + args.delta() < 0) { throw new IllegalStateException("insufficient funds"); } // Checkpointed write — idempotent conditional update keyed on a stable value. return ctx.run(Orders::applyBalanceDelta, args).await(); } ``` Because the read is checkpointed, a replay sees the snapshot the workflow already acted on, not a value another process changed in the meantime. ## Retries and terminal SoR errors Per-step retry behavior is set with `ctx.options(new Opts().withRetryPolicy(...))`. The Java SDK has **no non-retryable error wrapper** (unlike Go's `NewNonRetryable`) — control retries with the policy. For a 4xx that will never succeed, run the leaf under `new Retry.Never()` so a thrown error propagates immediately instead of being retried: ```java import io.resonatehq.resonate.Context.Opts; import io.resonatehq.resonate.Retry.Exponential; import io.resonatehq.resonate.Retry.Never; // Transient-tolerant step — retry with backoff: ctx.options(new Opts().withRetryPolicy(new Exponential(1, 5, 2, 30))) .run(Orders::chargeCard, charge).await(); // Validation step that must not be retried on a bad request: ctx.options(new Opts().withRetryPolicy(new Never())) .run(Orders::validateSku, sku).await(); ``` Inside the leaf, treat an idempotent-success response (e.g. a `409 Already Reserved` under the same key) as success and return the existing ID rather than throwing. ## Distinct Java idioms - **Type-keyed DI via `ctx.getDependency(Foo.class)`** — register once with `r.withDependency(new Foo())`. This is the idiomatic way to reach a DB pool / HTTP client / gateway inside a leaf; you do not close it over the workflow (the Go approach) or pass it as a serialized arg. - **`record` args bundle the leaf's inputs** — `ChargeArgs`, `ReserveArgs`, etc. keep each `ctx.run` to a single serializable value and document the boundary. - **Stable idempotency keys from `ctx.info().id()`** — stable across replays. `System.currentTimeMillis()` / `Math.random()` / `UUID.randomUUID()` differ across replays and defeat deduplication. - **Retry policy controls terminal-vs-retried**, since there is no `NonRetryable` — use `new Retry.Never()` for 4xx-style errors that should propagate immediately. - **Branch in plain Java between checkpointed steps** — the read and write are each a `ctx.run`; the `if` logic between them is pure and replay-safe. ## Avoid - **Bare external calls in workflow code.** Any DB query, HTTP call, or file write outside a `ctx.run` re-executes on every replay. Always wrap SoR interactions in a leaf. - **`System.currentTimeMillis()` / `Math.random()` / `UUID.randomUUID()` as idempotency-key inputs.** They differ across replays; the SoR sees a different key each time and cannot deduplicate. Derive keys from `ctx.info().id()` or stable args. - **Reading the SoR in bare workflow code.** The read fires again on replay and may observe a changed value. Checkpoint SoR reads in their own `ctx.run`. - **Passing the SoR client as a workflow arg.** It is not JSON-serializable. Inject it with `r.withDependency` / `ctx.getDependency` instead. - **Writing to two systems without a clear SoR.** Designate one SoR; make the other a compensatable side effect. For multi-system compensation, see `resonate-saga-pattern-java`. ## Related skills - `resonate-basic-durable-world-usage-java` — `ctx.run`, `Opts`, `ctx.getDependency`, `ctx.info`, the replay model - `resonate-saga-pattern-java` — when the SoR doesn't cover all steps and you need compensation across services - `resonate-basic-debugging-java` — idempotency-key bugs, generic decode (`List<LinkedHashMap>`) from by-name results, retry exhaustion - `durable-execution` — foundational replay semantics; this pattern is checkpoint-centric - `resonate-external-system-of-record-pattern-typescript` — language-agnostic mental model - `resonate-external-system-of-record-pattern-python` — the closest sibling; the Java DI API mirrors Python
Ver no GitHub