Async Trait Patterns
Use this recipe when designing async traits, repositories, service abstractions, and test doubles.
Native async fn in Traits
Use native async fn in traits for static dispatch and concrete types:
pub trait Repository: Send + Sync {
async fn find_by_id(&self, id: &str) -> Result<Option<Item>>;
async fn save(&self, item: &Item) -> Result<()>;
}This is concise and works well when callers use generic type parameters:
pub struct UserService<R> {
repository: R,
}
impl<R> UserService<R>
where
R: Repository,
{
pub async fn load(&self, id: &str) -> Result<Option<Item>> {
self.repository.find_by_id(id).await
}
}Dynamic Dispatch
Use async-trait only when dyn Trait, Box<dyn Trait>, or trait-object storage is required:
async-trait = "0.1"use async_trait::async_trait;
#[async_trait]
pub trait Repository: Send + Sync {
async fn find_by_id(&self, id: &str) -> Result<Option<Item>>;
}
#[async_trait]
impl Repository for Box<dyn Repository> {
async fn find_by_id(&self, id: &str) -> Result<Option<Item>> {
self.as_ref().find_by_id(id).await
}
}Public Library Traits
For public libraries where Send bounds and object safety matter, prefer explicit future return types or boxed futures rather than assuming native async fn will fit every caller:
use std::future::Future;
pub trait Repository {
fn find_by_id(
&self,
id: &str,
) -> impl Future<Output = Result<Option<Item>>> + Send;
}For trait objects:
use std::future::Future;
use std::pin::Pin;
pub trait Repository: Send + Sync {
fn find_by_id<'a>(
&'a self,
id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Option<Item>>> + Send + 'a>>;
}Rules
- Prefer static dispatch first.
- Use
async-traitdeliberately, not by habit. - Be explicit about
Sendwhen code will run on a multi-threaded executor. - Keep trait APIs small; avoid turning traits into broad service locators.
- For tests, prefer small traits with focused responsibilities over mocking large clients.