Skip to content

Commit f1f87b3

Browse files
Promote new API and deprecate codegen based API (#628)
* Deprecate codegen based API * Rename *Handle methods to to*
1 parent b4308fe commit f1f87b3

40 files changed

Lines changed: 680 additions & 484 deletions

File tree

MIGRATION.md

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# Migrating from the codegen API to the reflection-based API
2+
3+
The **codegen API** — the `sdk-api-gen` annotation processor / `sdk-api-kotlin-gen` KSP generator, with
4+
handlers taking a `Context` (or `ObjectContext`/`WorkflowContext`/…) first parameter — is **deprecated**
5+
and will be removed in a future release. Prefer the **reflection-based API**: no annotation processor, and
6+
handlers drop the `Context` parameter for the static `dev.restate.sdk.Restate` methods (Java) or the
7+
top-level functions in `dev.restate.sdk.kotlin` (Kotlin).
8+
9+
Both use the **same** annotations (`@Service`, `@Handler`, …), so migrating is mostly removing the
10+
`Context` parameter and changing how you invoke other services. The two styles coexist, so you can migrate
11+
one service at a time.
12+
13+
---
14+
15+
## TL;DR — API mapping
16+
17+
### Java: `Context` API → `Restate` API
18+
19+
| Codegen / `Context` API | Reflection-based `Restate` API |
20+
|-------------------------------------------------|-----------------------------------------------------------------------------------|
21+
| `void greet(Context ctx, String req)` | `void greet(String req)` (drop the `Context` parameter) |
22+
| `ctx.run(...)` / `ctx.runAsync(...)` | `Restate.run(...)` / `Restate.runAsync(...)` |
23+
| `ctx.random()` | `Restate.random()` |
24+
| `ctx.sleep(d)` / `ctx.timer(...)` | `Restate.sleep(d)` / `Restate.timer(...)` |
25+
| `ctx.instantNow()` | `Restate.instantNow()` |
26+
| `ctx.awakeable(...)` / `ctx.awakeableHandle(id)`| `Restate.awakeable(...)` / `Restate.awakeableHandle(id)` |
27+
| `ctx.signal(...)` | `Restate.signal(...)` |
28+
| `ctx.get(key)` / `ctx.set(key, v)` / `ctx.clear(key)` | `Restate.state().get(key)` / `Restate.state().set(key, v)` / `Restate.state().clear(key)` |
29+
| `ctx.key()` | `Restate.key()` |
30+
| `ctx.promise(key)` / `ctx.promiseHandle(key)` | `Restate.promise(key)` / `Restate.promiseHandle(key)` |
31+
| `ctx.invocationHandle(id, ...)` | `Restate.invocationHandle(id, ...)` |
32+
| Code-generated clients (Service) | `Restate.service(Class)` / `Restate.toService(Class)` |
33+
| Code-generated clients (Virtual Object) | `Restate.virtualObject(Class, key)` / `Restate.toVirtualObject(Class, key)` |
34+
| Code-generated clients (Workflow) | `Restate.workflow(Class, key)` / `Restate.toWorkflow(Class, key)` |
35+
36+
From **outside** a handler (the ingress client), the equivalents live on `dev.restate.client.Client`:
37+
`client.service(Class)` / `client.toService(Class)` / `client.virtualObject(Class, key)` / etc.
38+
39+
### Kotlin: `Context` API → top-level functions
40+
41+
| Codegen / `Context` API | Reflection-based top-level functions |
42+
|-----------------------------------------------------|---------------------------------------------------------|
43+
| `suspend fun greet(ctx: Context, req: String)` | `suspend fun greet(req: String)` (drop the `Context`) |
44+
| `ctx.runBlock { ... }` / `ctx.runAsync { ... }` | `runBlock { ... }` / `runAsync { ... }` |
45+
| `ctx.random()` | `random()` |
46+
| `ctx.sleep(d)` / `ctx.timer(...)` | `sleep(d)` / `timer(...)` |
47+
| `ctx.awakeable<T>()` / `ctx.awakeableHandle(id)` | `awakeable<T>()` / `awakeableHandle(id)` |
48+
| `ctx.signal<T>(name)` | `signal<T>(name)` |
49+
| `ctx.get(key)` / `ctx.set(key, v)` / `ctx.clear(key)` | `state().get(key)` / `state().set(key, v)` / `state().clear(key)` |
50+
| `ctx.key()` | `objectKey()` / `workflowKey()` |
51+
| `ctx.promise(key)` / `ctx.promiseHandle(key)` | `promise(key)` / `promiseHandle(key)` |
52+
| Code-generated clients (Service) | `service<T>()` / `toService<T>()` |
53+
| Code-generated clients (Virtual Object) | `virtualObject<T>(key)` / `toVirtualObject<T>(key)` |
54+
| Code-generated clients (Workflow) | `workflow<T>(key)` / `toWorkflow<T>(key)` |
55+
56+
All the top-level functions are in the `dev.restate.sdk.kotlin` package — add `import dev.restate.sdk.kotlin.*`.
57+
58+
---
59+
60+
## Java migration
61+
62+
### 1. Remove the annotation processor dependency
63+
64+
Delete the `sdk-api-gen` annotation processor from your build; the reflection-based API needs no processor.
65+
66+
```diff
67+
- annotationProcessor("dev.restate:sdk-api-gen:<version>")
68+
implementation("dev.restate:sdk-java-http:<version>")
69+
```
70+
71+
### 2. Remove the `Context` parameter from your handlers
72+
73+
Remove `Context` / `ObjectContext` / `SharedObjectContext` / `WorkflowContext` / `SharedWorkflowContext`
74+
from your `@Handler`-annotated methods. The same applies to interfaces annotated with Restate annotations.
75+
76+
```java
77+
// Before
78+
@VirtualObject
79+
public class Counter {
80+
@Handler
81+
public void add(ObjectContext ctx, long request) {}
82+
83+
@Shared
84+
@Handler
85+
public long get(SharedObjectContext ctx) {}
86+
}
87+
88+
// After
89+
@VirtualObject
90+
public class Counter {
91+
@Handler
92+
public void add(long request) {}
93+
94+
@Shared
95+
@Handler
96+
public long get() {}
97+
}
98+
```
99+
100+
### 3. Replace `ctx.` calls with `Restate.`
101+
102+
```java
103+
// Before
104+
@Handler
105+
public void add(ObjectContext ctx, long value) {
106+
long currentValue = ctx.get(TOTAL).orElse(0L);
107+
ctx.set(TOTAL, currentValue + value);
108+
}
109+
110+
// After
111+
@Handler
112+
public void add(long value) {
113+
var state = Restate.state();
114+
long currentValue = state.get(TOTAL).orElse(0L);
115+
state.set(TOTAL, currentValue + value);
116+
}
117+
```
118+
119+
### 4. Replace code-generated clients
120+
121+
**Simple proxy (direct calls):**
122+
123+
```java
124+
// Direct method call on a virtual object
125+
Restate.virtualObject(Counter.class, "my-key").add(1);
126+
```
127+
128+
**Handle-based (advanced patterns):**
129+
130+
```java
131+
// call() with a method reference returns a DurableFuture you can await and/or compose
132+
int count = Restate.toVirtualObject(Counter.class, "my-counter")
133+
.call(Counter::increment)
134+
.await();
135+
136+
// send() for one-way invocation without waiting
137+
InvocationHandle<Integer> handle = Restate.toVirtualObject(Counter.class, "my-counter")
138+
.send(Counter::increment);
139+
140+
// Invocation options such as an idempotency key
141+
int idempotentCount = Restate.toVirtualObject(Counter.class, "my-counter")
142+
.call(Counter::increment, InvocationOptions.idempotencyKey("my-idempotency-key"))
143+
.await();
144+
```
145+
146+
---
147+
148+
## Kotlin migration
149+
150+
### 1. Remove the KSP code generator dependency
151+
152+
```diff
153+
- ksp("dev.restate:sdk-api-kotlin-gen:<version>")
154+
implementation("dev.restate:sdk-kotlin-http:<version>")
155+
```
156+
157+
You can also remove the `com.google.devtools.ksp` Gradle plugin if it's no longer used elsewhere.
158+
159+
### 2. Remove the `Context` parameter from your handlers
160+
161+
```kotlin
162+
// Before
163+
@VirtualObject
164+
class Counter {
165+
@Handler
166+
suspend fun add(ctx: ObjectContext, value: Long) {}
167+
168+
@Shared
169+
@Handler
170+
suspend fun get(ctx: SharedObjectContext): Long {}
171+
}
172+
173+
// After
174+
import dev.restate.sdk.kotlin.*
175+
176+
@VirtualObject
177+
class Counter {
178+
@Handler
179+
suspend fun add(value: Long) {}
180+
181+
@Shared
182+
@Handler
183+
suspend fun get(): Long {}
184+
}
185+
```
186+
187+
### 3. Replace `ctx.` calls with the top-level functions
188+
189+
```kotlin
190+
// Before
191+
@Handler
192+
suspend fun add(ctx: ObjectContext, value: Long) {
193+
val currentValue = ctx.get(TOTAL) ?: 0L
194+
ctx.set(TOTAL, currentValue + value)
195+
}
196+
197+
// After
198+
@Handler
199+
suspend fun add(value: Long) {
200+
val state = state()
201+
val currentValue = state.get(TOTAL) ?: 0L
202+
state.set(TOTAL, currentValue + value)
203+
}
204+
```
205+
206+
### 4. Replace code-generated clients
207+
208+
**Simple proxy (direct calls):**
209+
210+
```kotlin
211+
virtualObject<Counter>("my-key").add(1) // Direct method call
212+
```
213+
214+
**Handle-based (advanced patterns):**
215+
216+
```kotlin
217+
// call() with a lambda returns a DurableFuture you can await and/or compose
218+
val count = toVirtualObject<Counter>("my-counter")
219+
.request { add(1) }
220+
.call()
221+
.await()
222+
223+
// send() for one-way invocation without waiting
224+
val handle = toVirtualObject<Counter>("my-counter")
225+
.request { add(1) }
226+
.send()
227+
228+
// Invocation options such as an idempotency key
229+
val idempotentCount = toVirtualObject<Counter>("my-counter")
230+
.request { add(1) }
231+
.options { idempotencyKey = "my-idempotency-key" }
232+
.call()
233+
.await()
234+
```
235+
236+
### Kotlin gotcha: proxy clients need non-final classes
237+
238+
The proxy clients (`service<T>()`, `virtualObject<T>(key)`, `toService<T>()`, …) create a runtime proxy of
239+
`T`. Kotlin classes are `final` by default, which prevents proxy generation.
240+
241+
> **Using Kotlin + Spring Boot?** You don't need any of the below — the Restate Spring Boot Kotlin starter
242+
> already applies the all-open plugin for the Restate annotations for you.
243+
244+
Otherwise, pick one of:
245+
246+
- **Define an interface** carrying the Restate annotations, implement it, and use the interface type for
247+
proxies: `service<MyServiceInterface>()`. (Recommended.)
248+
- Mark the annotated classes/methods `open`.
249+
- Apply the [Kotlin all-open compiler plugin](https://kotlinlang.org/docs/all-open-plugin.html):
250+
251+
```kotlin
252+
plugins {
253+
kotlin("plugin.allopen") version "<kotlin-version>"
254+
}
255+
256+
allOpen {
257+
annotations(
258+
"dev.restate.sdk.annotation.Service",
259+
"dev.restate.sdk.annotation.VirtualObject",
260+
"dev.restate.sdk.annotation.Workflow")
261+
}
262+
```
263+
264+
This makes any class annotated with a Restate annotation (and its methods) `open` automatically.
265+
266+
---
267+
268+
## Deterministic time
269+
270+
Use the deterministic clock instead of `Instant.now()` / `Clock.System.now()`:
271+
272+
```java
273+
// Java
274+
Instant now = Restate.instantNow();
275+
```
276+
277+
```kotlin
278+
// Kotlin — requires opting in to the experimental kotlin.time API
279+
import kotlin.time.Clock
280+
import kotlin.time.ExperimentalTime
281+
import dev.restate.sdk.kotlin.*
282+
283+
@OptIn(ExperimentalTime::class)
284+
@Handler
285+
suspend fun myHandler(): String {
286+
val now = Clock.Restate.now()
287+
return "Current time: $now"
288+
}
289+
```

0 commit comments

Comments
 (0)