A Complete DI Example — Putting It All Together

A small blog API with three services, a provider graph, and production vs test wiring.

Domain

#![allow(unused)]
fn main() {
struct User { id: u64, name: String, email: String }
struct Post { id: u64, author_id: u64, title: String, body: String }
enum AppError { Db(DbError), Notify(NotifyError) }
}

Three service traits + keys

#![allow(unused)]
fn main() {
use id_effect::Effect;
use std::sync::Arc;

pub trait UserRepository: Send + Sync {
    fn get_user(&self, id: u64) -> Effect<User, DbError, ()>;
}

pub trait PostRepository: Send + Sync {
    fn get_posts_by_author(&self, author_id: u64) -> Effect<Vec<Post>, DbError, ()>;
}

pub trait NotificationService: Send + Sync {
    fn send_welcome(&self, to: &str) -> Effect<(), NotifyError, ()>;
}
struct UserRepo;
struct PostRepo;
struct Notifier;
}

Business logic

#![allow(unused)]
fn main() {
use id_effect::{effect, require, caps, succeed};

fn get_author_feed(author_id: u64) -> Effect<(User, Vec<Post>), AppError, caps!(UserRepo, PostRepo)> {
    effect!(|r| {
        let user_repo = ~UserRepo;
        let post_repo = ~PostRepo;
        let user  = ~ user_repo.get_user(author_id).map_error(AppError::Db);
        let posts = ~ post_repo.get_posts_by_author(author_id).map_error(AppError::Db);
        (user, posts)
    })
}

fn register_user(name: &str, email: &str) -> Effect<User, AppError, caps!(UserRepo, Notifier)> {
    effect!(|r| {
        let repo = ~UserRepo;
        let notifier = ~Notifier;
        let user = ~ repo.create_user(name, email).map_error(AppError::Db);
        ~ notifier.send_welcome(&user.email).map_error(AppError::Notify);
        user
    })
}
}

Production wiring

use id_effect::{provide, run_with};

fn main() {
    run_with(
        [
            provide!(ConfigLive),
            provide!(DatabaseLive),
            provide!(PgUserRepoLive),
            provide!(PgPostRepoLive),
            provide!(SmtpNotifierLive),
        ],
        get_author_feed(1),
    )
    .expect("app failed");
}

CapabilityGraph ensures DatabaseLive runs before repo providers that read Database from Env.

Test wiring

#![allow(unused)]
fn main() {
#[test]
fn feed_includes_authors_posts() {
    let mut env = Env::new();
    env.insert::<Cap<UserRepo>>(Arc::new(mock_user_repo(&[alice(), bob()])));
    env.insert::<Cap<PostRepo>>(Arc::new(mock_post_repo(&[alice_post()])));

    let (_user, posts) = run_test(get_author_feed(1), env).unwrap();
    assert_eq!(posts.len(), 1);
    assert_eq!(posts[0].title, "Alice's Post");
}
}

What this demonstrates

  • Business logic declares caps!(…) and uses ~Key — no Postgres, SMTP, or concrete types in domain code.
  • Providers swap at the edge via provide!(…).
  • The dependency graph is explicit in provider requires() + the effect's capability list.

That's compile-time dependency injection: requirements are typed; wiring is centralized at main and in tests.