Skip to content

Commit d12e782

Browse files
committed
docs: add an SDK roadmap for deferred features, open decisions, and codegen
1 parent ea0cc81 commit d12e782

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

docs/roadmap.md

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# SDK Roadmap
2+
3+
This document tracks confirmed-but-deferred enhancements, open design decisions, and the
4+
code-generation effort. Each item links to its tracking issue and records a recommended
5+
direction and its dependencies, so the work can be sequenced rather than picked up ad hoc.
6+
7+
Bug fixes, documentation, CI, and the smaller self-contained enhancements are handled directly
8+
as pull requests and are not listed here. The items below are larger features, decisions that
9+
should be made before the relevant code is written, or design work for a generator that does not
10+
yet exist.
11+
12+
## Status legend
13+
14+
- **Planned** — confirmed worth doing; deferred only for scope/sequencing.
15+
- **Decision needed** — a real fork with material trade-offs; pick a direction before writing code.
16+
- **Design** — forward-looking design for the (not-yet-started) code generator.
17+
18+
---
19+
20+
## Track A — Async parity
21+
22+
The synchronous pipeline ships retry, bearer auth, and pagination; the asynchronous side does
23+
not yet have equivalents. These bring the async path up to parity.
24+
25+
### Async retry step at the RETRY stage — [#31] (Planned, large)
26+
No `AsyncHttpStep` occupies `Stage.RETRY`, so async pipelines get no retry. Add a public
27+
`AsyncRetryStep : AsyncHttpStep` with `final override val stage = Stage.RETRY` mirroring the sync
28+
`RetryStep`, plus a concrete `DefaultAsyncRetryStep` that reuses `HttpRetryOptions` and a
29+
`ScheduledExecutorService` for non-blocking backoff (no thread parked during the delay). Reuse the
30+
existing classification/`Retry-After` logic. Should land after the retry-defaults reconciliation
31+
(#38) so both async and sync share one backoff source of truth.
32+
33+
### Async bearer-auth with background token refresh — [#32] (Planned, medium)
34+
Add an async bearer-auth `AsyncHttpStep` and a non-blocking refresh path on the token provider
35+
(`fetchAsync(...) : CompletableFuture<BearerToken>`, defaulting to wrapping the blocking `fetch`),
36+
so a valid-but-near-expiry token is returned immediately while the refresh runs off-thread.
37+
Coordinate with the bearer-token eviction work (#33) so the sync and async steps share eviction
38+
and refresh semantics.
39+
40+
### Async pagination — [#34] (Planned, large)
41+
Both pagination surfaces are blocking today. After the pagination unification decision (#30),
42+
add an async sibling that drives an iterative `executeAsync` re-arm, closing each page on
43+
completion, with `Flow`/`Flux` bridges living in the coroutines/reactor adapter modules (not in
44+
`sdk-core`). Blocked on #30.
45+
46+
---
47+
48+
## Track B — Core capabilities
49+
50+
Confirmed features deferred for scope. None is a defect; each adds a genuinely missing capability.
51+
52+
### Shared instrumentation emitter — [#26] (Planned, medium)
53+
The sync and async instrumentation steps duplicate the emit/redact/preview/metrics logic, with a
54+
standing extraction marker. The two copies have already diverged once. Extract the stateless logic
55+
into an internal `InstrumentationEmitters` constructed from the values both steps need
56+
(`HttpInstrumentationOptions`, `ClientLogger`, `Clock`, the lazy metric instruments). Pure internal
57+
refactor — high value for preventing future drift between the two steps.
58+
59+
### Per-call options channel on the request context — [#27] (Planned, medium)
60+
`RequestContext` carries no per-call override channel. Add an immutable `RequestOptions`
61+
(per-phase timeout overlay, response-validation toggle, ad-hoc credential override) with
62+
`applyDefaults` merge semantics, keeping the transport SPIs single-method. Depends on the
63+
`Timeout` value type from #41.
64+
65+
### Per-phase `Timeout` value type — [#41] (Planned, medium)
66+
No core timeout type exists; per-phase timeouts are configured ad hoc per transport. Add an
67+
immutable `Timeout` (connect / read / write / request `Duration`s, read/write defaulting to
68+
request) that adapters translate to native settings, kept distinct from the retry total-timeout.
69+
Prerequisite for #27.
70+
71+
### AutoCloseable SSE stream — [#35] (Planned, medium)
72+
The SSE surface is a bare `Sequence`, so a partially-consumed stream can leak the response. Add an
73+
`SseStream : AutoCloseable, Iterable<ServerSentEvent>` that owns the `Response` and closes it on
74+
stream close or partial consumption — mirroring the close-on-partial-consume invariant pagination
75+
already enforces.
76+
77+
### Multipart request body — [#61] (Planned, large)
78+
No multipart support today. Add a public `MultipartRequestBody : RequestBody` (immutable + Builder)
79+
with one shared frame-size function driving both `writeTo` and `contentLength`, file parts
80+
streaming zero-copy via `FileRequestBody` and non-file parts encoded through the `Serde` SPI (no
81+
Jackson in `sdk-core`).
82+
83+
### Opt-in resource leak detector — [#45] (Planned, medium)
84+
No leak detection exists. Add an internal, opt-in, log-only `LeakDetector` that WARNs when a
85+
caller-owned closeable becomes phantom-reachable unclosed, using a reflectively-obtained
86+
`java.lang.ref.Cleaner` so Java-8 bytecode no-ops on JDK 8 and the whole thing is gated behind a
87+
system property. Never auto-closes.
88+
89+
---
90+
91+
## Track C — Decisions to make
92+
93+
These need a recorded decision (and sometimes a small spike) before any code is written.
94+
95+
### URL model: resolved `java.net.URL` vs deconstructed — [#29] (Decision needed)
96+
**Recommended:** keep a single opaque URL field on `Request` (do not explode into
97+
scheme/host/port/segments/query on the immutable model), but migrate the stored type from
98+
`java.net.URL` to `java.net.URI``URI` is parse-only, DNS-free, and already what the JDK
99+
transport needs. Preserve the existing textual, DNS-free equality. Record the decision in
100+
`docs/architecture.md`. Gates #28 and the typed-page generation in #56.
101+
102+
### First-class `QueryParams` multimap — [#28] (Planned, medium; gated on #29)
103+
`QueryParam` is a `TODO()` stub and `RequestRebuilder` does query-string surgery by splitting on
104+
`&` with single-value semantics. Add a public `QueryParams` multimap modeled on `Headers`
105+
(insertion-ordered, multi-value, explicit encoding rules). Lands after the URL-model decision so it
106+
projects into the chosen type.
107+
108+
### Unify the two pagination stacks — [#30] (Decision needed, large)
109+
Two parallel pagination surfaces exist and are both public API: `http.paging`
110+
(`PagedIterable`/`PagedResponse`, BYO fetcher) and `pagination` (`Paginator`/`Page` + strategies,
111+
self-driving). **Recommended:** make the strategy-driven `Paginator` + `Page` the canonical
112+
surface (it owns the client, matches peer SDKs, and is the model the typed-page generation in #56
113+
builds on) and deprecate the other. A public-API redesign — decide before coding. Gates #34.
114+
115+
### Deep-array value-equality utility — [#49] (Decision needed, small)
116+
A `contentEquals`/`contentHashCode` helper has no consumer in the current tree; it is tied to the
117+
future generated DTOs (with `ByteArray` fields). **Recommended:** defer until codegen needs it, to
118+
avoid adding public surface that immediately churns the API snapshot and the coverage floor with no
119+
caller. Fold into the codegen runtime when that lands.
120+
121+
### VirtualThreads close-event log level — [#10] (Decision needed, trivial)
122+
The `executor.closed` event is emitted at DEBUG (not INFO). **Recommended:** keep DEBUG (it matches
123+
every other async-adapter event and keeps clean-shutdown noise off INFO) and close the issue,
124+
optionally noting the intent in the `close()` KDoc. No code change beyond the optional doc line.
125+
126+
### Release automation — [#75] (Decision needed, gated on CI)
127+
Versioning, changelog, and publishing are manual, and the version string is duplicated across ~10
128+
build scripts. **Recommended (once CI lands):** record a decision on adopting release-please; if
129+
adopted, collapse the duplicated version to a single anchor and add a release workflow + Sonatype
130+
publishing. Gated on CI (#70) and complements the publishing-convention-plugin work (#71).
131+
132+
---
133+
134+
## Track D — Code generation
135+
136+
The SDK is deliberately a hand-written HTTP-client toolkit; **there is no generator in the tree
137+
yet.** Issues #50#69 are design for a future KotlinPoet-based generator that would emit typed
138+
clients over this toolkit. None is a defect, and most cannot become code until the generator and
139+
its keystone runtime type (#50) exist. They are captured here (and several warrant short design-doc
140+
sections now) so the effort can start coherently.
141+
142+
### Keystone: dependency-free four-state JSON field model — [#50] (Design, large)
143+
The foundation the rest of the model generation builds on: a dep-free sealed `JsonField<T>`
144+
(`Known` / `Missing` / `Null` / `Raw`) plus a `RawJson` tree in `sdk-core`, with all
145+
Jackson↔`RawJson` conversion confined to the `sdk-serde-jackson` adapter — mirroring how `Tristate`
146+
is split today. Almost every other codegen item depends on this. **Land a design doc first**
147+
(`docs/codegen-json-field.md`), then the runtime type, then the generator template.
148+
149+
### Generated model shape (all depend on #50)
150+
- **Thin models over a hand-written runtime — [#51]** (Design): generated models stay <100 lines
151+
(fields + accessors) while the runtime owns the forward-compat machinery.
152+
- **`additionalProperties` pass-through — [#52]** (Design): capture unknown fields into an immutable
153+
`Map<String, RawJson>` so read-modify-write round-trips don't drop server-added fields.
154+
- **Unions as private-ctor + per-variant accessors + visitor — [#53]** (Design): Java-8-safe
155+
`oneOf` emission with a retained raw node and an `unknown(raw)` fallback.
156+
- **Forward-compatible enums — [#54]** (Design): open value + known/value pair so deserialization
157+
never throws on an unrecognized server value.
158+
- **Discriminator/const fields as defaulted raw values with dual accessors — [#65]** (Design):
159+
fold into the #50 design effort.
160+
- **Optional `validate()`/`isValid()`/`validity()` triad — [#64]** (Design): opt-in, memoized,
161+
off the deserialize path; used only as last-resort union disambiguation behind a discriminator.
162+
163+
### Generated service/client shape
164+
- **Two-tier raw/cooked service methods — [#55]** (Design): a "cooked" method returning the parsed
165+
body and a "raw" method returning a lazy `ParsedResponse<T>`. Builds on the `ResponseHandler`
166+
seam (#36, now in review) and the operation-params SPI (#57).
167+
- **Minimal `OperationParams` SPI — [#57]** (Design): projects an operation's inputs into
168+
headers/query/path/body and feeds the context chain. Gated on the `QueryParams` multimap (#28).
169+
- **Curated operation overload set — [#58]** (Design): one canonical method per operation plus a
170+
small curated overload set leaning on Kotlin default arguments, rather than the full overload
171+
cross-product.
172+
- **Lazy sub-service accessor tree — [#59]** (Design): `by lazy` sub-service accessors on a
173+
generated root client, reusing a nested raw-response impl.
174+
- **`withOptions(Consumer<Builder>)` returning a new immutable client — [#60]** (Design): gated on
175+
first deciding whether to introduce a single cloneable client-config with `toBuilder()`.
176+
- **Typed page classes that rebuild typed params — [#56]** (Design, xlarge): `nextPage()`
177+
re-invokes the operation with a typed param object, not a spliced URL string. Gated on #57, #28,
178+
and the pagination unification (#30).
179+
- **Per-endpoint SSE adapter — [#62]** (Design): maps an `SseStream` to a lazily-decoded
180+
`Iterable<TModel>`. Gated on #35 and #36.
181+
- **Per-operation auth descriptors with a precedence ladder — [#63]** (Design): the generator emits
182+
an `AuthMetadata` per operation; `sdk-core`'s auth step consumes it scheme-agnostically. The
183+
scheme-agnostic primitives partly exist already.
184+
185+
### Generator plumbing & outputs
186+
- **Strict structured-output JSON-schema encoding rules — [#66]** (Design, small): capture the
187+
encoding contract (all-required + `additionalProperties:false` + optional-as-nullable-union) as a
188+
design-doc section now; it is adapter-only, never `sdk-core`.
189+
- **Reusable fail-soft recursive validator skeleton — [#67]** (Design, small): a small generic
190+
recursion-guarded, path-prefixed validator idiom for the generator's own IR; build in codegen
191+
week 1–2, once the IR exists.
192+
- **Provenance file stamped into generated SDKs — [#68]** (Design, small): generator version +
193+
input-contract hash, emitted into generated output only, never into the hand-written toolkit.
194+
- **Spring Boot starter per generated API — [#69]** (Design, medium): an optional sibling
195+
`<api>-spring-boot-starter` with `@ConfigurationProperties`, a customizer `fun interface`, and an
196+
`@AutoConfiguration` assembling {IoProvider + transport + HttpPipeline}, keeping Spring out of
197+
`sdk-core` and the generated client.
198+
199+
### Suggested codegen sequencing
200+
201+
1. Design docs: #50 (keystone), #66, #67, #68 — these can be written now, before any generator code.
202+
2. Land #50's runtime type in `sdk-core`; decide #29 (URL model) and #28 (`QueryParams`).
203+
3. Stand up the generator IR + the fail-soft validator (#67), then model emission (#51#54, #64, #65).
204+
4. Service/client emission (#55, #57, #58, #59, #60), then pagination/SSE/auth generation (#56, #62, #63).
205+
5. Packaging outputs (#68 provenance, #69 Spring starter).

0 commit comments

Comments
 (0)