Skip to content

New struct-based service definition API#117

Merged
slinkydeveloper merged 6 commits into
mainfrom
struct-api
Jul 20, 2026
Merged

New struct-based service definition API#117
slinkydeveloper merged 6 commits into
mainfrom
struct-api

Conversation

@slinkydeveloper

@slinkydeveloper slinkydeveloper commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Before

#[restate_sdk::service]
trait Greeter {
    async fn greet(name: String) -> HandlerResult<String>;
}

struct GreeterImpl;

impl Greeter for GreeterImpl {
    async fn greet(&self, _ctx: Context<'_>, name: String) -> HandlerResult<String> {
        Ok(format!("Greetings {name}"))
    }
}

Endpoint::builder().bind(GreeterImpl.serve()).build();

After

struct Greeter;

#[restate_sdk::service]
impl Greeter {
    #[handler]
    async fn greet(&self, _ctx: Context<'_>, name: String) -> HandlerResult<String> {
        Ok(format!("Greetings {name}"))
    }
}

Endpoint::builder().bind(Greeter).build();

Virtual object (shared/exclusive inferred from the context type)

struct Counter;

#[restate_sdk::object]
impl Counter {
    #[handler]
    async fn add(&self, ctx: ObjectContext<'_>, val: u64) -> HandlerResult<u64> {
        let new = ctx.get::<u64>("count").await?.unwrap_or(0) + val;
        ctx.set("count", new);
        Ok(new)
    }

    #[handler]
    async fn get(&self, ctx: SharedObjectContext<'_>) -> HandlerResult<u64> {
        Ok(ctx.get::<u64>("count").await?.unwrap_or(0))
    }
}

Workflow

struct SignupWorkflow;

#[restate_sdk::workflow]
impl SignupWorkflow {
    #[handler]
    async fn run(&self, ctx: WorkflowContext<'_>, email: String) -> HandlerResult<bool> {
        let click = ctx.promise::<String>("email.clicked").await?;
        Ok(!click.is_empty())
    }

    #[handler]
    async fn click(&self, ctx: SharedWorkflowContext<'_>, secret: String) -> HandlerResult<()> {
        ctx.resolve_promise::<String>("email.clicked", secret);
        Ok(())
    }
}

Dependency injection via &self struct fields

struct RunExample(reqwest::Client);

#[restate_sdk::service]
impl RunExample {
    #[handler]
    async fn do_run(&self, ctx: Context<'_>) -> HandlerResult<Json<HashMap<String, String>>> {
        let res = ctx
            .run(|| async move {
                Ok(Json(self.0.get("https://httpbin.org/ip").send().await?.json().await?))
            })
            .await?;
        Ok(res)
    }
}

Endpoint::builder().bind(RunExample(reqwest::Client::new())).build();

Overriding names and client visibility

struct Greeter;

#[restate_sdk::service(name = "greeter", client_visibility = "pub(crate)")]
impl Greeter {
    #[handler(name = "myGreet")]
    async fn my_greet(&self, _ctx: Context<'_>, name: String) -> HandlerResult<String> {
        Ok(name)
    }
}

Generic services (type params, bounds, where-clauses)

struct Repo<D: Database> {
    db: D,
}

#[restate_sdk::object]
impl<D> Repo<D>
where
    D: Database + Send + Sync + 'static,
{
    #[handler]
    async fn get(&self, ctx: SharedObjectContext<'_>) -> HandlerResult<String> {
        Ok(self.db.load(ctx.key()).await)
    }
}

The trait-based API still works, now deprecated

warning: use of deprecated constant: The trait-based service API is deprecated;
         put #[restate_sdk::service] (or #[object]/#[workflow]) on an impl block instead.
 --> src/main.rs:4:1
  |
4 | #[restate_sdk::service]
  | ^^^^^^^^^^^^^^^^^^^^^^^

🤖 Generated with Claude Code

slinkydeveloper and others added 3 commits July 20, 2026 15:59
Put #[restate_sdk::service] (or #[object]/#[workflow]) on an impl block,
annotate handlers with #[handler], and do dependency injection via &self
struct fields. Bind the struct value directly (no .serve()) via the new
IntoServiceDefinition trait. Shared/exclusive is inferred from the context
type. Generic structs (bounds + where-clauses) are supported.

The trait-based API keeps working but is deprecated with a compile-time
warning. Examples and test-services migrated to the new API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The service/object/workflow/handler macros are now re-exported from the
prelude, so first-party examples, tests and test-services use the bare
attribute form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

Test Results

  8 files  ±0    8 suites  ±0   2m 34s ⏱️ -2s
 43 tests ±0   43 ✅ ±0  0 💤 ±0  0 ❌ ±0 
186 runs  ±0  186 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 3d928fd. ± Comparison against base commit 845fe1b.

♻️ This comment has been updated with latest results.

@slinkydeveloper slinkydeveloper linked an issue Jul 20, 2026 that may be closed by this pull request
@slinkydeveloper
slinkydeveloper merged commit f52d297 into main Jul 20, 2026
4 checks passed
@slinkydeveloper
slinkydeveloper deleted the struct-api branch July 20, 2026 14:28
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A more ergonomic macro

1 participant