aws / aws/aws-durable-execution-sdk-rust
Rename Serdes and split one-way codec capabilities
- Dominant language
- Rust
- Stars
- 13
- Forks
- 0
- Avg merge
- 10m
- Merged PRs (30d)
- 1
Description
## Summary
`Serdes` sounds like a passive serialization/deserialization abstraction, but its actual contract is a bidirectional codec and, on successful operation paths, an application-visible transformation boundary.
For a step result, the current flow is:
```text
step body returns T
-> Serdes::serialize(T) -> String
-> checkpoint String
-> Serdes::deserialize(String) -> T
-> return the reconstructed T
```
The value returned during the live execution is therefore the decoded value, not necessarily the value produced by the step body. This behavior is deliberate because it gives live execution and replay the same result, but the current name does not communicate it.
Relevant API and execution paths:
- [`Serdes`](https://github.com/aws/aws-durable-execution-sdk-rust/blob/60ea9cf07652872083f0e6dac9b4761a801168ea/src/serdes.rs#L46-L72)
- [step success round-trip](https://github.com/aws/aws-durable-execution-sdk-rust/blob/60ea9cf07652872083f0e6dac9b4761a801168ea/src/step.rs#L488-L555)
- [invoke payload uses only serialization](https://github.com/aws/aws-durable-execution-sdk-rust/blob/60ea9cf07652872083f0e6dac9b4761a801168ea/src/builders/invoke.rs#L137-L170)
- [callback payload uses only deserialization](https://github.com/aws/aws-durable-execution-sdk-rust/blob/60ea9cf07652872083f0e6dac9b4761a801168ea/src/builders/callback.rs#L109-L120)
## Problem
### The name establishes the wrong mental model
Rust users generally expect serialization to change representation while preserving the application value. A custom `Serdes` can instead normalize or otherwise transform the value returned by the first execution:
```text
serialize(" Hello ") -> "hello"
deserialize("hello") -> "hello"
```
The step body produced `" Hello "`, but the operation returns `"hello"`. `Codec` better communicates that encode/decode behavior and the round-trip contract determine the application-visible value.
### One trait combines capabilities that operations do not all need
Some operations use only one direction:
- invoke input needs encoding only;
- invoke result and callback result need decoding only;
- step, child, map, parallel, and wait-for-condition state need both directions.
Nevertheless, every implementation must provide both `serialize` and `deserialize`. This creates meaningless methods for one-way use cases and makes trait bounds less precise than the operation behavior.
### The round-trip law is implicit
For correct and unsurprising behavior, a codec should satisfy an explicit law such as:
```text
decode(encode(value)) is application-equivalent to value
```
If canonicalizing or lossy codecs are supported, the documentation must instead clearly state that the decoded representation is authoritative on both live execution and replay.
## Minimum acceptable change
If the current ownership and execution model remains unchanged:
1. Rename `Serdes` to `Codec`.
2. Rename `serialize` / `deserialize` to `encode` / `decode`.
3. Rename `JsonSerdes`, `FileSystemSerdes`, `SerdesContext`, and builder `.serdes(...)` methods consistently.
4. Document prominently that successful live values are encoded and immediately decoded before being returned.
5. Document the required round-trip law, including whether canonicalizing transformations are supported.
6. Add a test demonstrating that live execution and replay return the same decoded value for a transforming codec.
If compatibility must be preserved, the existing names can remain temporarily as deprecated aliases and forwarding methods. The crate is currently an experimental pre-1.0 preview, so making the naming correction before stabilization would be preferable.
## Recommended design
Split the directional capabilities while retaining a convenient combined bound:
```rust
trait Encoder: Send + Sync + 'static {
fn encode(
&self,
value: T,
context: CodecContext,
) -> impl Future> + Send;
}
trait Decoder: Send + Sync + 'static {
fn decode(
&self,
wire: String,
context: CodecContext,
) -> impl Future> + Send;
}
trait Codec: Encoder + Decoder {}
```
Then apply the narrowest bound at each API boundary:
- invoke input: `Encoder`;
- invoke and callback result: `Decoder`;
- checkpointed operation values and carried state: `Codec`.
The exact ownership signature can remain as it is initially. A separate follow-up can evaluate splitting synchronous value encoding from asynchronous payload storage, which could allow conventional borrowed serialization without requiring every `T` to be `Sync` or immediately decoding a newly produced value.
## Why this should be done
- Names define the behavior users assume before reading detailed documentation.
- `Codec`, `Encoder`, and `Decoder` are established terms that match the actual capabilities.
- Narrow traits prevent implementations from supplying methods that cannot be meaningfully used.
- Compiler errors become clearer: an invoke payload reports that it needs an encoder rather than a full serializer/deserializer.
- The public API gains room to evolve encoding and storage independently.
- The live/replay value contract becomes explicit instead of an implementation detail discovered from the execution code.
## Acceptance criteria
- [ ] Public naming accurately communicates codec semantics.
- [ ] The live encode/checkpoint/decode behavior is documented.
- [ ] The round-trip or canonicalization contract is explicit.
- [ ] One-way operations require only their actual directional capability.
- [ ] Existing default JSON and filesystem implementations continue to work.
- [ ] Tests cover a transforming codec on both live execution and replay.
- [ ] README and examples use the revised terminology consistently.
Contributor guide
Research direction
Start with src/serdes.rs and trace the step success round-trip in src/step.rs, then inspect the one-way bounds in src/builders/invoke.rs and src/builders/callback.rs. Review README and examples for terminology. Done means the public names and directional bounds are consistent, the live/replay contract is documented, default implementations still work, and tests cover a transforming codec.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design, developer-experience
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 38/100