Release 2.6.0
We're excited to announce Restate Java SDK 2.6.0!
Virtual Threads by default (Java 21+)
The default executor for Java services now uses virtual threads on Java 21+, falling back to Executors.newCachedThreadPool() for older Java versions.
You can still customize the executor providing HandlerRunner.Options.withExecutor() to Endpoint#bind, or for Spring users, use the new Spring properties (restate.executor or restate.components.<name>.executor).
Note: This change only applies to Java services. Kotlin services continue to Dispatchers.Default as default coroutine dispatcher.
[Spring Boot] Configure services using Spring's Properties
You can now configure Restate services directly from Spring's application.properties or application.yml files. This provides a centralized way to configure service behavior without modifying code.
Example Configuration
# Global executor for all services (Java only)
restate.executor=myExecutorBean
# Configuration for a service named "MyService"
restate.components.MyService.executor=myServiceExecutor
restate.components.MyService.inactivity-timeout=10m
restate.components.MyService.abort-timeout=1m
restate.components.MyService.idempotency-retention=7d
restate.components.MyService.journal-retention=1d
restate.components.MyService.ingress-private=false
restate.components.MyService.enable-lazy-state=true
restate.components.MyService.documentation=My service description
restate.components.MyService.metadata.version=1.0
restate.components.MyService.metadata.team=platform
restate.components.MyService.retry-policy.initial-interval=100ms
restate.components.MyService.retry-policy.exponentiation-factor=2.0
restate.components.MyService.retry-policy.max-interval=10s
restate.components.MyService.retry-policy.max-attempts=10
restate.components.MyService.retry-policy.on-max-attempts=PAUSE
# Per-handler configuration
restate.components.MyService.handlers.myHandler.inactivity-timeout=5m
restate.components.MyService.handlers.myHandler.ingress-private=true
restate.components.MyService.handlers.myHandler.documentation=Handler description
restate.components.MyService.handlers.myWorkflowHandler.workflow-retention=30dAnnotation based configuration
The configuration attribute on @RestateService, @RestateVirtualObject, and @RestateWorkflow annotations now accepts a bean name of type RestateComponentProperties (previously RestateServiceConfigurator):
@RestateService(configuration = "myServiceConfig")
public class MyService {
// ...
}
@Bean
public RestateComponentProperties myServiceConfig() {
return new RestateComponentProperties()
.setInactivityTimeout(Duration.ofMinutes(10))
.setDocumentation("My service");
}[Kotlin] New experimental API
Following the introduction of the new experimental API for Java in v2.5.0, we're now bringing the same experience to Kotlin!
The new API removes the need for the KSP code generator, making it significantly simpler to use the Restate SDK:
- No KSP code generator required: The SDK now uses reflection to discover and bind your services, eliminating the need for build-time code generation and making your build simpler and faster.
- No more Context parameters: Handler methods no longer need to accept a
Contextparameter. Instead, use top-level functions fromdev.restate.sdk.kotlinto access state, promises, and other Restate functionality. - Cleaner method signatures: Your handler methods now have cleaner signatures that focus on your business logic rather than Restate infrastructure.
The new API is opt-in and can be incrementally adopted in an existing project, meaning the existing API will continue to work as usual.
It is marked as experimental and subject to change in the next releases.
We're looking for your feedback to evolve it and stabilize it!
How to use it
-
Remove the KSP code generator dependency
sdk-api-kotlin-gen -
Remove the
Context,ObjectContext,SharedObjectContext,WorkflowContext,SharedWorkflowContextparameters from your@Handlerannotated methods. For example:@VirtualObject class Counter { @Handler suspend fun add(ctx: ObjectContext, value: Long) {} @Shared @Handler suspend fun get(ctx: SharedObjectContext): Long {} }
Becomes:
import dev.restate.sdk.kotlin.* @VirtualObject class Counter { @Handler suspend fun add(value: Long) {} @Shared @Handler suspend fun get(): Long {} }
The same applies for interfaces using Restate annotations.
-
Replace all the usages of
ctx.with the top-level functions. For example:@Handler suspend fun add(ctx: ObjectContext, value: Long) { val currentValue = ctx.get(TOTAL) ?: 0L val newValue = currentValue + value ctx.set(TOTAL, newValue) }
Becomes:
@Handler suspend fun add(value: Long) { val state = state() val currentValue = state.get(TOTAL) ?: 0L val newValue = currentValue + value state.set(TOTAL, newValue) }
-
Replace all the usages of code-generated clients. There are two ways to invoke services:
Simple proxy (for direct calls):
The top-level functions let you create proxies to call services directly using
service<T>(),virtualObject<T>(key)andworkflow<T>(key):virtualObject<Counter>("my-key").add(1) // Direct method call
Handle-based (for advanced patterns):
For asynchronous handling, request composition, or invocation options (such as idempotency keys), use
toService<T>(),toVirtualObject<T>(key)andtoWorkflow<T>(key):// Use call() with a lambda to return a DurableFuture you can await asynchronously and/or compose with other futures val count = toVirtualObject<Counter>("my-counter") .request { add(1) } .call() .await() // Use send() for one-way invocation without waiting val handle = toVirtualObject<Counter>("my-counter") .request { add(1) } .send() // Add request options such as idempotency key val count = toVirtualObject<Counter>("my-counter") .request { add(1) } .options { idempotencyKey = "my-idempotency-key" } .call() .await()
Reference sheet
Context API (2.5.x) |
New reflection API (2.6.0) |
|---|---|
ctx.runBlock { ... }/ctx.runAsync { ... } |
runBlock { ... }/runAsync { ... } |
ctx.random() |
random() |
ctx.timer() |
timer() |
ctx.awakeable() |
awakeable<T>() |
ctx.get(key) / ctx.set(key, value) |
state().get(key) / state().set(key, value) |
ctx.promise(key) |
promise(key) |
| Code generated clients (Services) | service<T>() / toService<T>() |
| Code generated clients (Virtual Objects) | virtualObject<T>(key) / toVirtualObject<T>(key) |
| Code generated clients (Workflows) | workflow<T>(key) / toWorkflow<T>(key) |
Using concrete classes with proxy clients
By default, Kotlin classes are final, preventing libraries like Restate to generate runtime client proxies.
We generally suggest one of the following three options:
- Define an interface with all Restate annotations and have your class implement it. Use the interface type for proxy calls, e.g.
service<MyServiceInterface>(). - Use
openin all Restate annotated classes/methods. - Setup the Kotlin allopen compiler plugin with the following configuration:
Gradle (Kotlin DSL)
plugins {
kotlin("plugin.allopen") version "<kotlin-version>"
}
allOpen {
annotations("dev.restate.sdk.annotation.Service", "dev.restate.sdk.annotation.VirtualObject", "dev.restate.sdk.annotation.Workflow")
}This configuration automatically makes any class annotated with Restate annotations open, along with all their methods.
Gradual migration
You can gradually migrate to the new API by disabling the KSP code generator for specific classes.
For example, if you have a project with a my.example.Greeter and a my.example.Counter, you can decide to migrate only my.example.Counter to use the new API.
To do so, keep the KSP code generator dependency and pass the KSP option dev.restate.codegen.disabledClasses as follows:
Gradle (Kotlin DSL)
ksp {
val disabledClassesCodegen =
listOf(
// Ignore Counter in the KSP code generator
"my.example.Counter")
arg("dev.restate.codegen.disabledClasses", disabledClassesCodegen.joinToString(","))
}New API for Deterministic Time
We've added a new API to get the current time deterministically that's consistent across replays.
Java
import java.time.Instant;
@Handler
public String myHandler() {
Instant now = Restate.instantNow();
// or using the context-based API:
// Instant now = ctx.instantNow();
return "Current time: " + now;
}Kotlin
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import dev.restate.sdk.kotlin.*
@OptIn(ExperimentalTime::class)
@Handler
suspend fun myHandler(): String {
val now = Clock.Restate.now()
return "Current time: $now"
}Note: in Kotlin you need to opt in the Kotlin experimental time APIs.
Other changes
- Fix detection of
@Rawannotation in Java Reflection API Serde.RAW.contentType()now correctly returnsapplication/octet-stream- Dependency updates:
- Jackson 2.19.4
- Spring Boot 3.4.13
- Vert.x 4.5.24
- OpenTelemetry 1.58.0
- JUnit 5.14.1
Full Changelog: v2.5.0...v2.6.0