Skip to content

Commit eddf871

Browse files
committed
docs: patterns
Replace best-practices.md with a structured Best practices section under Patterns. Each page covers one topic, carries TypeScript, Python, and Java examples, and uses inline admonitions (tip/warning/danger/info) in place of a trailing "Common mistakes" list. Pages - determinism.md Determinism during replay - idempotency.md Idempotency and retries - state.md Manage state - step-design.md Step design - pause-resume.md Pause and resume (waits, callbacks, polling) - code-organization.md Code organization All code examples are --8<-- transclusions of files under examples/{typescript,python,java}/patterns/<topic>/. Every SDK claim was verified against the TS, Python, and Java SDK sources; notes live at .kiro/specs/patterns-rewrite/verification.md. Structure - Add a Best practices subsection under Patterns with its own index. - Drop best-practices.md. - Drop examples/{typescript,python,java}/best-practices/ (all three were either placeholders or are now inlined into the new pages). - Expand the Patterns nav block in zensical.toml. Notable corrections to prior best-practices content - StepConfig field is semantics, not executionMode. - Python context.wait takes a Duration (Duration.from_hours(24)), not a raw int. - Drop the invented UnrecoverableError pattern; show the real retry- strategy error-filtering API per language. - Document the SDK's automatic ReplayChildren behavior for large BatchResults (>256 KB) and the per-item checkpoint consequences. - Handler input is stored once and read from execution state on every replay (not re-passed by the invoker). Closes #14 Closes #33 Closes #51 Closes #69
1 parent 0836424 commit eddf871

193 files changed

Lines changed: 1762 additions & 1567 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/patterns/best-practices.md

Lines changed: 0 additions & 1046 deletions
This file was deleted.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Code organization
2+
3+
Organize your code so that your workflows are composable and testable.
4+
5+
## Separate orchestration from business logic
6+
7+
Business logic is the work a step performs. Orchestration is when and in what order
8+
steps run.
9+
10+
Put your logic in its own testable functions that the durable operation calls, rather
11+
than inline in the operation. Keep `DurableContext` out of your domain logic.
12+
13+
=== "TypeScript"
14+
15+
```typescript
16+
--8<-- "examples/typescript/patterns/code-organization/separate-logic.ts"
17+
```
18+
19+
=== "Python"
20+
21+
```python
22+
--8<-- "examples/python/patterns/code-organization/separate-logic.py"
23+
```
24+
25+
=== "Java"
26+
27+
```java
28+
--8<-- "examples/java/patterns/code-organization/separate-logic.java"
29+
```
30+
31+
!!! tip
32+
33+
Simplify your unit testing by not referencing `DurableContext` in your domain logic
34+
functions. Cover orchestration separately with the
35+
[durable function testing framework](../../testing/index.md).
36+
37+
## Group operations with child contexts
38+
39+
A child context is a named scope that groups operations in the execution history. Inside
40+
a child context you can call any durable operation, including nested child contexts.
41+
42+
Use a child context when a block of work is one logical unit that is composed of several
43+
operations.
44+
45+
=== "TypeScript"
46+
47+
```typescript
48+
--8<-- "examples/typescript/patterns/code-organization/child-context.ts"
49+
```
50+
51+
=== "Python"
52+
53+
```python
54+
--8<-- "examples/python/patterns/code-organization/child-context.py"
55+
```
56+
57+
=== "Java"
58+
59+
```java
60+
--8<-- "examples/java/patterns/code-organization/child-context.java"
61+
```
62+
63+
## Group related configuration
64+
65+
When several steps share the same retry strategy, timeout, or serdes, define the
66+
configuration once and reuse it. Use the name of a configuration object to make its
67+
intent clear.
68+
69+
=== "TypeScript"
70+
71+
```typescript
72+
--8<-- "examples/typescript/patterns/code-organization/group-config.ts"
73+
```
74+
75+
=== "Python"
76+
77+
```python
78+
--8<-- "examples/python/patterns/code-organization/group-config.py"
79+
```
80+
81+
=== "Java"
82+
83+
```java
84+
--8<-- "examples/java/patterns/code-organization/group-config.java"
85+
```
86+
87+
## Run independent work concurrently
88+
89+
Use `parallel` for a fixed number of named branches and `map` to iterate over a
90+
variable-length list. Both are durable so each branch checkpoints independently and
91+
survives Lambda timeouts or sandbox crashes, unlike language-specific constructs such as
92+
`Promise.all`, `asyncio.gather`, or `CompletableFuture`.
93+
94+
=== "TypeScript"
95+
96+
```typescript
97+
--8<-- "examples/typescript/patterns/code-organization/parallelism.ts"
98+
```
99+
100+
=== "Python"
101+
102+
```python
103+
--8<-- "examples/python/patterns/code-organization/parallelism.py"
104+
```
105+
106+
=== "Java"
107+
108+
```java
109+
--8<-- "examples/java/patterns/code-organization/parallelism.java"
110+
```
111+
112+
See the [parallel](../../sdk-reference/operations/parallel.md) and
113+
[map](../../sdk-reference/operations/map.md) references for completion policies,
114+
concurrency limits, and per-item configuration.
115+
116+
## See also
117+
118+
- [Step design](step-design.md)
119+
- [Child context operation](../../sdk-reference/operations/child-context.md)
120+
- [Parallel operation](../../sdk-reference/operations/parallel.md)
121+
- [Map operation](../../sdk-reference/operations/map.md)
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Determinism during replay
2+
3+
The Durable Execution SDK checkpoints your code so that it can terminate the current
4+
invocation and not consume compute while it waits for a timed duration or processing
5+
result to be ready. The AWS Lambda backend re-invokes the function when it is ready to
6+
resume processing.
7+
8+
Durable functions run your handler from the top on every invocation. A step that
9+
completed in an earlier invocation returns its checkpointed result on replay without
10+
re-executing the code inside the step. Anything outside a step or other durable
11+
operation runs every time the function replays.
12+
13+
For replay to follow the same path, the code that runs every time has to produce the
14+
same values. That is determinism. Non-deterministic code inside your handler body can
15+
send control flow down a different branch on replay, so a downstream step runs with the
16+
wrong inputs.
17+
18+
## Handler code must be deterministic
19+
20+
Any code that is not inside a durable operation must be a pure function of the handler
21+
inputs and the results of completed operations. Anything that depends on wall-clock
22+
time, a random source, an external service, the local file system, or mutable global
23+
state is non-deterministic and must run inside a durable operation.
24+
25+
Concrete examples of code that is not deterministic:
26+
27+
- **Time and identity** `Date.now()`, `time.time()`, `Instant.now()`, `UUID` generation,
28+
or anything that returns a different value each call.
29+
- **External I/O** HTTP calls, database reads, AWS SDK calls, reading files.
30+
- **Random numbers** `Math.random()`, `random.random()`, `Random`.
31+
32+
## Non-deterministic code must be in a durable operation
33+
34+
A step checkpoints its return value. On replay the step returns the checkpointed value
35+
instead of running the underlying code. Wrapping a non-deterministic call in a step
36+
means the value will always be the result of the first successful completion of that
37+
code.
38+
39+
=== "TypeScript"
40+
41+
```typescript
42+
--8<-- "examples/typescript/patterns/determinism/non-deterministic-in-step.ts"
43+
```
44+
45+
=== "Python"
46+
47+
```python
48+
--8<-- "examples/python/patterns/determinism/non-deterministic-in-step.py"
49+
```
50+
51+
=== "Java"
52+
53+
```java
54+
--8<-- "examples/java/patterns/determinism/non-deterministic-in-step.java"
55+
```
56+
57+
Because the SDK checkpoints the result of `generate-transaction-id`, every replay sees
58+
the same `transactionId` and the charge step receives the same argument. Without the
59+
wrapper, `UUID.randomUUID()` would produce a new value on every replay and the
60+
downstream step would either double-charge or hit an idempotency error from the payment
61+
service.
62+
63+
!!! tip
64+
65+
Wrap every non-deterministic call inside a step.
66+
67+
## Pass data through return values, not closures
68+
69+
State outside a step resets to its initial value on replay. Steps return their cached
70+
results, but assignments, mutations, and pushes that happen outside steps run again on
71+
every invocation. Although this pattern looks like it works on the first invocation, it
72+
breaks as soon as the workflow replays after a crash or a wait.
73+
74+
=== "TypeScript"
75+
76+
```typescript
77+
--8<-- "examples/typescript/patterns/determinism/return-value-passing.ts"
78+
```
79+
80+
=== "Python"
81+
82+
```python
83+
--8<-- "examples/python/patterns/determinism/return-value-passing.py"
84+
```
85+
86+
=== "Java"
87+
88+
```java
89+
--8<-- "examples/java/patterns/determinism/return-value-passing.java"
90+
```
91+
92+
For processing a list of independent items,
93+
[map](/durable-execution/sdk-reference/operations/map/) is a simpler choice than an
94+
explicit loop. It runs the per-item operation in parallel, checkpoints each result, and
95+
returns a `BatchResult` you can reduce. Use an explicit loop when items depend on each
96+
other, such as a running total or chained transformations. Keep the loop deterministic.
97+
Each step must produce the same result on replay.
98+
99+
!!! danger
100+
101+
Mutating state outside a step fails silently. The first invocation looks correct. Replay
102+
resets the mutation while steps return their cached results.
103+
104+
## Keep branches stable across replay
105+
106+
Control flow decisions made outside steps must depend only on deterministic inputs. If
107+
an `if` or `switch` depends on something non-deterministic, replay can walk a different
108+
branch and attempt to return results from operations that never ran. Wrap the
109+
non-deterministic decision into a step and branch on the step's return value.
110+
111+
=== "TypeScript"
112+
113+
```typescript
114+
--8<-- "examples/typescript/patterns/determinism/stable-branches.ts"
115+
```
116+
117+
=== "Python"
118+
119+
```python
120+
--8<-- "examples/python/patterns/determinism/stable-branches.py"
121+
```
122+
123+
=== "Java"
124+
125+
```java
126+
--8<-- "examples/java/patterns/determinism/stable-branches.java"
127+
```
128+
129+
The same rule applies to reading from external services. Fetching a flag from a database
130+
outside a step risks replaying against a changed value. Fetch inside a step, branch on
131+
the returned value.
132+
133+
!!! warning
134+
135+
Feature flags, environment variables read at runtime, and configuration pulled from a
136+
remote store could all change between the first invocation and a replay. Capture the
137+
value inside a step so the evaluation criteria are stable.
138+
139+
## See also
140+
141+
- [Idempotency and retries](idempotency.md) How retry behavior interacts with
142+
determinism.
143+
- [Step design](step-design.md) Naming, granularity, and the step boundary.
144+
- [Step operation reference](../../sdk-reference/operations/step.md)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Idempotency and retries
2+
3+
Replay and retry can each run the same operation more than once. An operation with a
4+
side effect repeats the side effect on every run. Idempotency describes operations where
5+
the effect remains the same regardless of how many times they run.
6+
7+
## At-most-once vs at-least-once
8+
9+
A step has an execution semantic that controls what happens when an attempt fails to
10+
complete, for example when the Lambda sandbox dies, the network drops or the runtime
11+
hits the invocation timeout mid-invocation.
12+
13+
- **At-least-once per retry (default).** The SDK initiates a `START` checkpoint and then
14+
immediately runs the code inside the step without waiting for the checkpoint
15+
response. If the attempt does not complete due to an interruption, the step runs
16+
again on replay. This is safe only for idempotent operations.
17+
- **At-most-once per retry.** The SDK waits for confirmation that the `START` checkpoint
18+
committed before running the code inside the step. If the attempt does not complete
19+
due to an interruption, the SDK marks the step as interrupted on replay and raises a
20+
`StepInterrupted` error instead of re-executing.
21+
22+
Both semantics are per retry attempt. Neither guarantees the step runs exactly once
23+
across the entire workflow. A retry strategy that retries on failure will run the step
24+
again even under at-most-once per retry. To limit a step to a single execution attempt
25+
end-to-end, combine at-most-once with a no-retry strategy.
26+
27+
!!! tip
28+
29+
At-least-once is the default. Any interruption re-runs the step on replay, so the code
30+
inside must be safe to run more than once.
31+
32+
## Match the semantic with the side effect
33+
34+
- **At-least-once** is for idempotent operations, such as reads that do not mutate,
35+
writes to an upsert-capable store, calls that accept an idempotency key and anything
36+
safe to re-run.
37+
- **At-most-once** is for operations with external side effects such as charging a
38+
payment card, sending a one-shot SMS or a POST to a non-idempotent API.
39+
40+
=== "TypeScript"
41+
42+
```typescript
43+
--8<-- "examples/typescript/patterns/idempotency/choose-semantics.ts"
44+
```
45+
46+
=== "Python"
47+
48+
```python
49+
--8<-- "examples/python/patterns/idempotency/choose-semantics.py"
50+
```
51+
52+
=== "Java"
53+
54+
```java
55+
--8<-- "examples/java/patterns/idempotency/choose-semantics.java"
56+
```
57+
58+
!!! warning
59+
60+
At-most-once applies per attempt, not per workflow. Combine it with a no-retry strategy
61+
to guarantee the step runs exactly once end-to-end.
62+
63+
## Idempotency tokens
64+
65+
For external services that support an idempotency key, such as most modern payment APIs,
66+
generate a key inside a step once and pass it to every attempt of the side-effecting
67+
step. The external service deduplicates repeated requests with the same key, so even
68+
at-least-once retries are safe.
69+
70+
!!! warning
71+
72+
Generate the key *inside* a step. A key generated outside a step changes on replay,
73+
which defeats deduplication and doubles up on retry.
74+
75+
=== "TypeScript"
76+
77+
```typescript
78+
--8<-- "examples/typescript/patterns/idempotency/idempotency-tokens.ts"
79+
```
80+
81+
=== "Python"
82+
83+
```python
84+
--8<-- "examples/python/patterns/idempotency/idempotency-tokens.py"
85+
```
86+
87+
=== "Java"
88+
89+
```java
90+
--8<-- "examples/java/patterns/idempotency/idempotency-tokens.java"
91+
```
92+
93+
The same pattern applies to any operation that writes to an external store. Include the
94+
key in the write and rely on the store's deduplication.
95+
96+
!!! tip
97+
98+
When an idempotency-enabled API returns a duplicate-request error on retry, it usually
99+
means the first attempt already succeeded. Handle that error as success, not failure.
100+
101+
## Database retry patterns
102+
103+
When you own the database, you can make writes idempotent without tokens.
104+
105+
- **Conditional writes.** DynamoDB `PutItem` with `attribute_not_exists`, SQL
106+
`INSERT ... ON CONFLICT DO NOTHING`, or upserts keyed by a stable ID.
107+
- **Check then write in a single transaction.** Use a transaction that reads the current
108+
state and writes the new state atomically.
109+
- **Append-only logs.** Write events with a deterministic event ID. Readers deduplicate
110+
on ID.
111+
112+
Avoid `INSERT` without a uniqueness constraint, and avoid counter increments without a
113+
conditional check if a retry could apply them twice.
114+
115+
## See also
116+
117+
- [Determinism and replay](determinism.md) Why operations can run more than once to
118+
begin with.
119+
- [Step design](step-design.md) Retry strategy and the boundary between steps.
120+
- [Errors and retries](../../sdk-reference/error-handling/errors.md)

0 commit comments

Comments
 (0)