Skip to content

Instantly share code, notes, and snippets.

@JadenGeller
Created December 18, 2025 09:36
Show Gist options
  • Select an option

  • Save JadenGeller/62942ef871f83096aa6262565d00fb02 to your computer and use it in GitHub Desktop.

Select an option

Save JadenGeller/62942ef871f83096aa6262565d00fb02 to your computer and use it in GitHub Desktop.
Capability Environment Pattern

The Capability Environment Pattern

A compile-time dependency injection pattern for Rust with zero runtime cost.

The Problem

Services need other services:

impl OrderService {
    fn create(&self, user_id: &str) -> Order {
        // need to validate user exists... how?
        // need to save to database... how?
    }
}

Pass everything explicitly? Tedious. Every new dependency means updating constructors throughout the chain.

fn create_order(
    db: &Database,
    user_service: &UserService,
    inventory: &InventoryService,
    // ... it grows
)

Singletons? Easy to write, impossible to test.

Runtime DI container? Flexible, but errors at runtime.

What we want: Pass one thing. Declare what you need. Get compile-time safety. Zero overhead.


The Core Idea

Services don't hold their dependencies. They hold a reference to an environment and reach through it:

struct OrderService<'a, E> {
    env: &'a E,
}

impl<E: HasUser + HasDatabase> OrderService<'_, E> {
    fn create(&self, user_id: &str) -> Order {
        let user = self.env.user().find(user_id);  // reach through
        self.env.db().insert(...);                  // reach through
        // ...
    }
}

The environment is just a struct with your infrastructure:

struct App {
    db: Postgres,
    cache: Redis,
}

The Has* traits declare capability slots—"this environment can provide X":

trait HasDatabase {
    type DB: Database;
    fn db(&self) -> &Self::DB;
}

trait HasUser {
    type User<'a>: UserService where Self: 'a;
    fn user(&self) -> Self::User<'_>;
}

The environment fills those slots:

impl HasDatabase for App {
    type DB = Postgres;
    fn db(&self) -> &Self::DB { &self.db }
}

impl HasUser for App {
    type User<'a> = PostgresUser<'a, Self>;
    fn user(&self) -> Self::User<'_> { 
        PostgresUser { env: self } 
    }
}

Services are created on demand, borrow the environment, do their work, disappear. No ownership tangles.


Bounds Are Visibility

This is the key insight.

When you write:

struct OrderService<'a, E: HasUser + HasDatabase> {
    env: &'a E,
}

The bound E: HasUser + HasDatabase does two things:

  1. Requirement — OrderService needs these capabilities to function
  2. Visibility — OrderService can only see these capabilities

Even if App has a cache, payment processor, notification service—OrderService can't touch them. The capability isn't in the bounds, so it doesn't exist from OrderService's perspective.

impl<E: HasUser + HasDatabase> OrderService<'_, E> {
    fn create(&self) {
        self.env.user();   // ✓ can access
        self.env.db();     // ✓ can access
        self.env.cache();  // ✗ compile error — not in bounds
    }
}

The bound is an access control list, checked at compile time.


Testing

Create a different environment with different implementations:

struct TestEnv {
    db: TestDatabase,
}

impl HasUser for TestEnv {
    type User<'a> = MockUser;  // swap in a mock
    fn user(&self) -> Self::User<'_> { MockUser }
}

Business logic doesn't change:

fn checkout<E: HasUser + HasOrder>(env: &E, user_id: &str) {
    let user = env.user().find(user_id);
    env.order().create(user_id, items);
}

// Production
let app = App { db: Postgres::new(), cache: Redis::new() };
checkout(&app, "alice");

// Test — same function, different environment
let test = TestEnv { db: TestDatabase::new() };
checkout(&test, "alice");

Swap implementations surgically. Mock one service, keep others real. The wiring is in the environment, not scattered across call sites.


What About Cycles?

Because services borrow &env immutably, they can call each other:

impl<E: HasOrder + HasDatabase> UserService for PostgresUser<'_, E> {
    fn on_signup(&self, user: &User) {
        // User service calling Order service
        self.env.order().create_welcome_gift(user.id);
    }
}

impl<E: HasUser + HasDatabase> OrderService for StandardOrder<'_, E> {
    fn create(&self, user_id: &str) {
        // Order service calling User service
        self.env.user().find(user_id);
    }
}

No ownership cycle—services don't hold each other, they reach through env. It's just mutual recursion.

This isn't the point of the pattern. Cycles are simply not prevented. Whether you allow them is your architecture choice. If User calls Order calls User, you'll get infinite recursion. That's on you.


Enforcing Boundaries

If you want hard walls—services can't bypass repos, handlers can't touch the database directly—use separate environment types.

The insight: the environment you pass controls what's visible. Pass a narrower environment, get narrower access.

// Infrastructure layer
struct InfraEnv {
    db: Postgres,
    cache: Redis,
}
impl HasDatabase for InfraEnv { ... }
impl HasCache for InfraEnv { ... }

// Repository layer — can see infra
struct RepoEnv<'a> {
    infra: &'a InfraEnv,
}
impl HasUserRepo for RepoEnv<'_> {
    fn user_repo(&self) -> UserRepo<'_, InfraEnv> {
        UserRepo { env: self.infra }  // repos reach into infra
    }
}
// RepoEnv does NOT impl HasDatabase — repos can't be bypassed

// Service layer — can see repos
struct ServiceEnv<'a> {
    repos: &'a RepoEnv<'a>,
}
impl HasUser for ServiceEnv<'_> { ... }
// ServiceEnv does NOT impl HasUserRepo — can't skip down to repos

Each layer uses the same pattern internally—hold a reference, reach through. But each layer controls what it exposes upward.

The sharing must form a tree. Instantiate at the root, borrow downward:

InfraEnv (owns db, cache)
    │
    └──► RepoEnv (borrows &InfraEnv)
              │
              └──► ServiceEnv (borrows &RepoEnv)

No diamonds. No duplicated databases. Single source of truth, references fan out.

fn main() {
    let infra = InfraEnv { db: Postgres::new(), cache: Redis::new() };
    let repos = RepoEnv { infra: &infra };
    let services = ServiceEnv { repos: &repos };
    
    // handlers receive &ServiceEnv — can only see services
    handle_request(&services);
}

This is more boilerplate. Use it when you need hard walls. The flat single-environment approach is fine when you trust discipline.


The Link Shortcut

When multiple environments use the same implementation, you repeat yourself:

impl HasOrder for App {
    type Order<'a> = StandardOrder<'a, Self>;
    fn order(&self) -> Self::Order<'_> { StandardOrder { env: self } }
}

impl HasOrder for TestEnv {
    type Order<'a> = StandardOrder<'a, Self>;
    fn order(&self) -> Self::Order<'_> { StandardOrder { env: self } }
}

The Link* pattern factors this out:

trait LinkOrder: HasUser + HasDatabase + Sized {}

impl<T: LinkOrder> HasOrder for T {
    type Order<'a> = StandardOrder<'a, Self> where Self: 'a;
    fn order(&self) -> Self::Order<'_> { StandardOrder { env: self } }
}

Now environments just opt in:

impl LinkOrder for App {}
impl LinkOrder for TestEnv {}

The supertrait bounds (HasUser + HasDatabase) encode what StandardOrder needs—it reaches through env.user() and env.db(), so LinkOrder requires those capabilities.

Limitation: Rust's coherence rules prevent overlapping blanket impls. You can't have both impl<T: LinkPostgresUser> HasUser for T and impl<T: LinkMockUser> HasUser for T. When multiple implementations exist, wire Has* directly.

This is purely about reducing repetition. Skip it if it's not pulling its weight.


Complete Example

// === Domain ===

struct User { id: String, email: String }
struct Order { user_id: String, items: Vec<String> }

// === Capability Traits ===

trait Database {
    fn query_user(&self, id: &str) -> Option<User>;
    fn insert_order(&self, order: &Order);
}

trait UserService {
    fn find(&self, id: &str) -> Option<User>;
}

trait OrderService {
    fn create(&self, user_id: &str, items: Vec<String>) -> Option<Order>;
}

// === Has* Traits ===

trait HasDatabase {
    type DB: Database;
    fn db(&self) -> &Self::DB;
}

trait HasUser {
    type User<'a>: UserService where Self: 'a;
    fn user(&self) -> Self::User<'_>;
}

trait HasOrder {
    type Order<'a>: OrderService where Self: 'a;
    fn order(&self) -> Self::Order<'_>;
}

// === Implementations ===

struct PostgresUser<'a, E: HasDatabase> { env: &'a E }

impl<E: HasDatabase> UserService for PostgresUser<'_, E> {
    fn find(&self, id: &str) -> Option<User> {
        self.env.db().query_user(id)
    }
}

struct StandardOrder<'a, E: HasUser + HasDatabase> { env: &'a E }

impl<E: HasUser + HasDatabase> OrderService for StandardOrder<'_, E> {
    fn create(&self, user_id: &str, items: Vec<String>) -> Option<Order> {
        self.env.user().find(user_id)?;
        let order = Order { user_id: user_id.into(), items };
        self.env.db().insert_order(&order);
        Some(order)
    }
}

// === Link (optional DRY) ===

trait LinkOrder: HasUser + HasDatabase + Sized {}

impl<T: LinkOrder> HasOrder for T {
    type Order<'a> = StandardOrder<'a, Self> where Self: 'a;
    fn order(&self) -> Self::Order<'_> { StandardOrder { env: self } }
}

// === Production Environment ===

struct Postgres;
impl Database for Postgres {
    fn query_user(&self, id: &str) -> Option<User> {
        Some(User { id: id.into(), email: format!("{}@example.com", id) })
    }
    fn insert_order(&self, order: &Order) {
        println!("Inserted order for {}", order.user_id);
    }
}

struct App { db: Postgres }

impl HasDatabase for App {
    type DB = Postgres;
    fn db(&self) -> &Self::DB { &self.db }
}

impl HasUser for App {
    type User<'a> = PostgresUser<'a, Self>;
    fn user(&self) -> Self::User<'_> { PostgresUser { env: self } }
}

impl LinkOrder for App {}

// === Business Logic ===

fn checkout<E: HasUser + HasOrder>(env: &E, user_id: &str) {
    // Can see: env.user(), env.order()
    // Cannot see: env.db() — not in bounds
    if let Some(order) = env.order().create(user_id, vec!["item".into()]) {
        println!("Order created for {}", order.user_id);
    }
}

fn main() {
    let app = App { db: Postgres };
    checkout(&app, "alice");
}

Summary

Concept Purpose
Has* traits Capability slots — "can provide X"
Bounds on services Requirements AND visibility
Services hold &Env Reach through, no ownership tangles
Multiple environments Swap implementations for testing
Nested environments Enforce hard boundaries (optional)
Link* traits Reduce repetition (optional)

The core insight: services don't hold dependencies, they hold the environment and reach through. Trait bounds control what they can reach. The compiler enforces your architecture.


When To Use This

Good fit:

  • Complex service dependencies
  • Need surgical test mocking
  • Want compile-time safety
  • Care about zero runtime overhead

Probably overkill:

  • Few services (just pass explicitly)
  • Team unfamiliar with Rust generics
  • Runtime DI is working fine

The pattern doesn't impose an architecture. It gives you the mechanism to express whatever architecture you want, with compile-time enforcement of whatever boundaries you draw.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment