[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-22-dependency-injection-rust-service-locator":3},{"article":4,"author":51},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":16,"meta_description":17,"focus_keyword":18,"og_image":19,"canonical_url":19,"robots_meta":20,"created_at":15,"updated_at":15,"tags":21,"category_name":31,"related_articles":32},"d0000000-0000-0000-0000-000000000122","a0000000-0000-0000-0000-000000000006","Deep EVM #22: Dependency Injection in Rust — ServiceLocator, Arc, and Trait Objects","deep-evm-22-dependency-injection-rust-service-locator","Implement dependency injection in Rust without frameworks. Covers the composition root pattern, Arc\u003Cdyn Trait> vs generics, mock implementations for testing, and the ServiceLocator pattern.","## The DI Problem in Rust\n\nDependency injection is a fundamental design principle: a component should receive its dependencies from the outside rather than creating them internally. In Java or C#, DI frameworks like Spring or Autofac handle this automatically. Rust does not have a standard DI framework, and its ownership model makes naive DI patterns awkward.\n\nConsider a service that needs a database connection, a cache, and a logger:\n\n```rust\n\u002F\u002F Bad: hardcoded dependencies\nstruct UserService {\n    db: PgPool,          \u002F\u002F Concrete type, not mockable\n    cache: RedisClient,  \u002F\u002F Concrete type\n}\n\nimpl UserService {\n    fn new() -> Self {\n        \u002F\u002F Worst pattern: constructing own dependencies\n        Self {\n            db: PgPool::connect(\"postgres:\u002F\u002F...\").await.unwrap(),\n            cache: RedisClient::new(\"redis:\u002F\u002F...\").unwrap(),\n        }\n    }\n}\n```\n\nThis is untestable. You cannot run unit tests without a real PostgreSQL and Redis instance. Let us fix this with Rust-idiomatic DI.\n\n## Pattern 1: Trait Objects with Arc\n\nDefine behavior as traits, then accept `Arc\u003Cdyn Trait>` for runtime polymorphism:\n\n```rust\nuse std::sync::Arc;\nuse async_trait::async_trait;\n\n#[async_trait]\ntrait UserRepository: Send + Sync {\n    async fn find_by_id(&self, id: i64) -> anyhow::Result\u003COption\u003CUser>>;\n    async fn save(&self, user: &User) -> anyhow::Result\u003C()>;\n    async fn delete(&self, id: i64) -> anyhow::Result\u003C()>;\n}\n\n#[async_trait]\ntrait CacheStore: Send + Sync {\n    async fn get(&self, key: &str) -> anyhow::Result\u003COption\u003CVec\u003Cu8>>>;\n    async fn set(&self, key: &str, value: &[u8], ttl_secs: u64) -> anyhow::Result\u003C()>;\n    async fn invalidate(&self, key: &str) -> anyhow::Result\u003C()>;\n}\n\nstruct UserService {\n    repo: Arc\u003Cdyn UserRepository>,\n    cache: Arc\u003Cdyn CacheStore>,\n}\n\nimpl UserService {\n    fn new(\n        repo: Arc\u003Cdyn UserRepository>,\n        cache: Arc\u003Cdyn CacheStore>,\n    ) -> Self {\n        Self { repo, cache }\n    }\n\n    async fn get_user(&self, id: i64) -> anyhow::Result\u003COption\u003CUser>> {\n        \u002F\u002F Check cache first\n        let cache_key = format!(\"user:{}\", id);\n        if let Some(bytes) = self.cache.get(&cache_key).await? {\n            let user: User = serde_json::from_slice(&bytes)?;\n            return Ok(Some(user));\n        }\n\n        \u002F\u002F Cache miss — query database\n        let user = self.repo.find_by_id(id).await?;\n        if let Some(ref u) = user {\n            let bytes = serde_json::to_vec(u)?;\n            self.cache.set(&cache_key, &bytes, 300).await?;\n        }\n\n        Ok(user)\n    }\n}\n```\n\n### Production Implementations\n\n```rust\nstruct PostgresUserRepository {\n    pool: PgPool,\n}\n\n#[async_trait]\nimpl UserRepository for PostgresUserRepository {\n    async fn find_by_id(&self, id: i64) -> anyhow::Result\u003COption\u003CUser>> {\n        let user = sqlx::query_as::\u003C_, User>(\n            \"SELECT id, name, email FROM users WHERE id = $1\"\n        )\n        .bind(id)\n        .fetch_optional(&self.pool)\n        .await?;\n        Ok(user)\n    }\n\n    async fn save(&self, user: &User) -> anyhow::Result\u003C()> {\n        sqlx::query(\n            \"INSERT INTO users (id, name, email) VALUES ($1, $2, $3)\n             ON CONFLICT (id) DO UPDATE SET name = $2, email = $3\"\n        )\n        .bind(user.id)\n        .bind(&user.name)\n        .bind(&user.email)\n        .execute(&self.pool)\n        .await?;\n        Ok(())\n    }\n\n    async fn delete(&self, id: i64) -> anyhow::Result\u003C()> {\n        sqlx::query(\"DELETE FROM users WHERE id = $1\")\n            .bind(id)\n            .execute(&self.pool)\n            .await?;\n        Ok(())\n    }\n}\n\nstruct RedisCacheStore {\n    client: redis::Client,\n}\n\n#[async_trait]\nimpl CacheStore for RedisCacheStore {\n    async fn get(&self, key: &str) -> anyhow::Result\u003COption\u003CVec\u003Cu8>>> {\n        let mut conn = self.client.get_multiplexed_async_connection().await?;\n        let result: Option\u003CVec\u003Cu8>> = redis::cmd(\"GET\")\n            .arg(key)\n            .query_async(&mut conn)\n            .await?;\n        Ok(result)\n    }\n\n    async fn set(&self, key: &str, value: &[u8], ttl_secs: u64) -> anyhow::Result\u003C()> {\n        let mut conn = self.client.get_multiplexed_async_connection().await?;\n        redis::cmd(\"SETEX\")\n            .arg(key)\n            .arg(ttl_secs)\n            .arg(value)\n            .query_async(&mut conn)\n            .await?;\n        Ok(())\n    }\n\n    async fn invalidate(&self, key: &str) -> anyhow::Result\u003C()> {\n        let mut conn = self.client.get_multiplexed_async_connection().await?;\n        redis::cmd(\"DEL\").arg(key).query_async(&mut conn).await?;\n        Ok(())\n    }\n}\n```\n\n### Test Implementations\n\n```rust\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::collections::HashMap;\n    use tokio::sync::RwLock;\n\n    struct MockUserRepository {\n        users: RwLock\u003CHashMap\u003Ci64, User>>,\n    }\n\n    impl MockUserRepository {\n        fn new() -> Self {\n            Self {\n                users: RwLock::new(HashMap::new()),\n            }\n        }\n\n        fn with_users(users: Vec\u003CUser>) -> Self {\n            let map: HashMap\u003Ci64, User> = users\n                .into_iter()\n                .map(|u| (u.id, u))\n                .collect();\n            Self {\n                users: RwLock::new(map),\n            }\n        }\n    }\n\n    #[async_trait]\n    impl UserRepository for MockUserRepository {\n        async fn find_by_id(&self, id: i64) -> anyhow::Result\u003COption\u003CUser>> {\n            Ok(self.users.read().await.get(&id).cloned())\n        }\n\n        async fn save(&self, user: &User) -> anyhow::Result\u003C()> {\n            self.users.write().await.insert(user.id, user.clone());\n            Ok(())\n        }\n\n        async fn delete(&self, id: i64) -> anyhow::Result\u003C()> {\n            self.users.write().await.remove(&id);\n            Ok(())\n        }\n    }\n\n    struct MockCacheStore {\n        data: RwLock\u003CHashMap\u003CString, Vec\u003Cu8>>>,\n    }\n\n    impl MockCacheStore {\n        fn new() -> Self {\n            Self {\n                data: RwLock::new(HashMap::new()),\n            }\n        }\n    }\n\n    #[async_trait]\n    impl CacheStore for MockCacheStore {\n        async fn get(&self, key: &str) -> anyhow::Result\u003COption\u003CVec\u003Cu8>>> {\n            Ok(self.data.read().await.get(key).cloned())\n        }\n\n        async fn set(&self, key: &str, value: &[u8], _ttl: u64) -> anyhow::Result\u003C()> {\n            self.data.write().await.insert(key.to_string(), value.to_vec());\n            Ok(())\n        }\n\n        async fn invalidate(&self, key: &str) -> anyhow::Result\u003C()> {\n            self.data.write().await.remove(key);\n            Ok(())\n        }\n    }\n\n    #[tokio::test]\n    async fn test_get_user_cache_miss() {\n        let user = User { id: 1, name: \"Alice\".into(), email: \"alice@test.com\".into() };\n        let repo = Arc::new(MockUserRepository::with_users(vec![user.clone()]));\n        let cache = Arc::new(MockCacheStore::new());\n        let service = UserService::new(repo, cache.clone());\n\n        let result = service.get_user(1).await.unwrap();\n        assert_eq!(result, Some(user));\n\n        \u002F\u002F Verify it was cached\n        assert!(cache.data.read().await.contains_key(\"user:1\"));\n    }\n}\n```\n\n## Pattern 2: Generics (Zero-Cost Abstraction)\n\nFor performance-critical paths where dynamic dispatch overhead matters, use generics:\n\n```rust\nstruct UserService\u003CR: UserRepository, C: CacheStore> {\n    repo: R,\n    cache: C,\n}\n\nimpl\u003CR: UserRepository, C: CacheStore> UserService\u003CR, C> {\n    fn new(repo: R, cache: C) -> Self {\n        Self { repo, cache }\n    }\n\n    async fn get_user(&self, id: i64) -> anyhow::Result\u003COption\u003CUser>> {\n        \u002F\u002F Same logic, but statically dispatched\n        \u002F\u002F Compiler monomorphizes for each (R, C) combination\n    }\n}\n```\n\nTrade-offs:\n\n| Aspect | Arc\u003Cdyn Trait> | Generics |\n|--------|---------------|----------|\n| Runtime cost | vtable lookup (~1ns) | Zero |\n| Binary size | Smaller | Larger (monomorphization) |\n| Flexibility | Can swap at runtime | Fixed at compile time |\n| Compile time | Faster | Slower |\n| Ergonomics | Simpler signatures | Generic bounds propagate |\n\nFor most applications, `Arc\u003Cdyn Trait>` is the right choice. The vtable lookup cost is negligible compared to the database or network calls your service makes. Use generics only in hot loops where nanoseconds matter.\n\n## The Composition Root Pattern\n\nWire everything together at the application entry point:\n\n```rust\n#[tokio::main]\nasync fn main() -> anyhow::Result\u003C()> {\n    \u002F\u002F Initialize infrastructure\n    let db_pool = PgPool::connect(&std::env::var(\"DATABASE_URL\")?).await?;\n    let redis = redis::Client::open(std::env::var(\"REDIS_URL\")?)?;\n\n    \u002F\u002F Build repositories\n    let user_repo: Arc\u003Cdyn UserRepository> = Arc::new(\n        PostgresUserRepository { pool: db_pool.clone() }\n    );\n    let cache: Arc\u003Cdyn CacheStore> = Arc::new(\n        RedisCacheStore { client: redis }\n    );\n\n    \u002F\u002F Build services\n    let user_service = Arc::new(UserService::new(\n        user_repo.clone(),\n        cache.clone(),\n    ));\n\n    \u002F\u002F Build app state\n    let state = AppState {\n        user_service,\n        \u002F\u002F ... other services\n    };\n\n    \u002F\u002F Start server\n    let app = Router::new()\n        .route(\"\u002Fusers\u002F:id\", get(get_user_handler))\n        .with_state(state);\n\n    let listener = tokio::net::TcpListener::bind(\"0.0.0.0:3001\").await?;\n    axum::serve(listener, app).await?;\n    Ok(())\n}\n```\n\nAll dependencies are created in `main()` and passed downward. No global state, no service locators called at arbitrary points. This is the purest form of DI.\n\n## The ServiceLocator Pattern\n\nFor complex systems with dozens of services, passing every dependency through constructors becomes unwieldy. The ServiceLocator pattern provides a registry that components can query:\n\n```rust\nuse std::any::{Any, TypeId};\nuse std::collections::HashMap;\nuse std::sync::Arc;\n\n#[derive(Clone, Default)]\nstruct ServiceLocator {\n    services: Arc\u003CHashMap\u003CTypeId, Box\u003Cdyn Any + Send + Sync>>>,\n}\n\nimpl ServiceLocator {\n    fn builder() -> ServiceLocatorBuilder {\n        ServiceLocatorBuilder {\n            services: HashMap::new(),\n        }\n    }\n\n    fn get\u003CT: 'static + Send + Sync>(&self) -> Option\u003C&T> {\n        self.services\n            .get(&TypeId::of::\u003CT>())\n            .and_then(|boxed| boxed.downcast_ref::\u003CT>())\n    }\n\n    fn resolve\u003CT: 'static + Send + Sync>(&self) -> &T {\n        self.get::\u003CT>()\n            .unwrap_or_else(|| panic!(\n                \"Service not registered: {}\",\n                std::any::type_name::\u003CT>()\n            ))\n    }\n}\n\nstruct ServiceLocatorBuilder {\n    services: HashMap\u003CTypeId, Box\u003Cdyn Any + Send + Sync>>,\n}\n\nimpl ServiceLocatorBuilder {\n    fn register\u003CT: 'static + Send + Sync>(mut self, service: T) -> Self {\n        self.services.insert(TypeId::of::\u003CT>(), Box::new(service));\n        self\n    }\n\n    fn build(self) -> ServiceLocator {\n        ServiceLocator {\n            services: Arc::new(self.services),\n        }\n    }\n}\n```\n\nUsage:\n\n```rust\nlet locator = ServiceLocator::builder()\n    .register::\u003CArc\u003Cdyn UserRepository>>(Arc::new(pg_repo))\n    .register::\u003CArc\u003Cdyn CacheStore>>(Arc::new(redis_cache))\n    .register::\u003CArc\u003CUserService>>(Arc::new(user_service))\n    .build();\n\n\u002F\u002F Later, in a handler:\nlet user_service = locator.resolve::\u003CArc\u003CUserService>>();\n```\n\nThe ServiceLocator trades compile-time safety for runtime flexibility. It is a controlled anti-pattern: use it at the composition root to simplify wiring, but prefer constructor injection within individual services.\n\n## Conclusion\n\nDependency injection in Rust does not require a framework. Traits define contracts, `Arc\u003Cdyn Trait>` provides runtime polymorphism, and the composition root wires everything together. Use mock implementations for testing, generics for hot paths, and the ServiceLocator pattern only when constructor chains become unmanageable. The key principle is the same across all languages: depend on abstractions, not concretions.","\u003Ch2 id=\"the-di-problem-in-rust\">The DI Problem in Rust\u003C\u002Fh2>\n\u003Cp>Dependency injection is a fundamental design principle: a component should receive its dependencies from the outside rather than creating them internally. In Java or C#, DI frameworks like Spring or Autofac handle this automatically. Rust does not have a standard DI framework, and its ownership model makes naive DI patterns awkward.\u003C\u002Fp>\n\u003Cp>Consider a service that needs a database connection, a cache, and a logger:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F Bad: hardcoded dependencies\nstruct UserService {\n    db: PgPool,          \u002F\u002F Concrete type, not mockable\n    cache: RedisClient,  \u002F\u002F Concrete type\n}\n\nimpl UserService {\n    fn new() -&gt; Self {\n        \u002F\u002F Worst pattern: constructing own dependencies\n        Self {\n            db: PgPool::connect(\"postgres:\u002F\u002F...\").await.unwrap(),\n            cache: RedisClient::new(\"redis:\u002F\u002F...\").unwrap(),\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This is untestable. You cannot run unit tests without a real PostgreSQL and Redis instance. Let us fix this with Rust-idiomatic DI.\u003C\u002Fp>\n\u003Ch2 id=\"pattern-1-trait-objects-with-arc\">Pattern 1: Trait Objects with Arc\u003C\u002Fh2>\n\u003Cp>Define behavior as traits, then accept \u003Ccode>Arc&lt;dyn Trait&gt;\u003C\u002Fcode> for runtime polymorphism:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::sync::Arc;\nuse async_trait::async_trait;\n\n#[async_trait]\ntrait UserRepository: Send + Sync {\n    async fn find_by_id(&amp;self, id: i64) -&gt; anyhow::Result&lt;Option&lt;User&gt;&gt;;\n    async fn save(&amp;self, user: &amp;User) -&gt; anyhow::Result&lt;()&gt;;\n    async fn delete(&amp;self, id: i64) -&gt; anyhow::Result&lt;()&gt;;\n}\n\n#[async_trait]\ntrait CacheStore: Send + Sync {\n    async fn get(&amp;self, key: &amp;str) -&gt; anyhow::Result&lt;Option&lt;Vec&lt;u8&gt;&gt;&gt;;\n    async fn set(&amp;self, key: &amp;str, value: &amp;[u8], ttl_secs: u64) -&gt; anyhow::Result&lt;()&gt;;\n    async fn invalidate(&amp;self, key: &amp;str) -&gt; anyhow::Result&lt;()&gt;;\n}\n\nstruct UserService {\n    repo: Arc&lt;dyn UserRepository&gt;,\n    cache: Arc&lt;dyn CacheStore&gt;,\n}\n\nimpl UserService {\n    fn new(\n        repo: Arc&lt;dyn UserRepository&gt;,\n        cache: Arc&lt;dyn CacheStore&gt;,\n    ) -&gt; Self {\n        Self { repo, cache }\n    }\n\n    async fn get_user(&amp;self, id: i64) -&gt; anyhow::Result&lt;Option&lt;User&gt;&gt; {\n        \u002F\u002F Check cache first\n        let cache_key = format!(\"user:{}\", id);\n        if let Some(bytes) = self.cache.get(&amp;cache_key).await? {\n            let user: User = serde_json::from_slice(&amp;bytes)?;\n            return Ok(Some(user));\n        }\n\n        \u002F\u002F Cache miss — query database\n        let user = self.repo.find_by_id(id).await?;\n        if let Some(ref u) = user {\n            let bytes = serde_json::to_vec(u)?;\n            self.cache.set(&amp;cache_key, &amp;bytes, 300).await?;\n        }\n\n        Ok(user)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Production Implementations\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct PostgresUserRepository {\n    pool: PgPool,\n}\n\n#[async_trait]\nimpl UserRepository for PostgresUserRepository {\n    async fn find_by_id(&amp;self, id: i64) -&gt; anyhow::Result&lt;Option&lt;User&gt;&gt; {\n        let user = sqlx::query_as::&lt;_, User&gt;(\n            \"SELECT id, name, email FROM users WHERE id = $1\"\n        )\n        .bind(id)\n        .fetch_optional(&amp;self.pool)\n        .await?;\n        Ok(user)\n    }\n\n    async fn save(&amp;self, user: &amp;User) -&gt; anyhow::Result&lt;()&gt; {\n        sqlx::query(\n            \"INSERT INTO users (id, name, email) VALUES ($1, $2, $3)\n             ON CONFLICT (id) DO UPDATE SET name = $2, email = $3\"\n        )\n        .bind(user.id)\n        .bind(&amp;user.name)\n        .bind(&amp;user.email)\n        .execute(&amp;self.pool)\n        .await?;\n        Ok(())\n    }\n\n    async fn delete(&amp;self, id: i64) -&gt; anyhow::Result&lt;()&gt; {\n        sqlx::query(\"DELETE FROM users WHERE id = $1\")\n            .bind(id)\n            .execute(&amp;self.pool)\n            .await?;\n        Ok(())\n    }\n}\n\nstruct RedisCacheStore {\n    client: redis::Client,\n}\n\n#[async_trait]\nimpl CacheStore for RedisCacheStore {\n    async fn get(&amp;self, key: &amp;str) -&gt; anyhow::Result&lt;Option&lt;Vec&lt;u8&gt;&gt;&gt; {\n        let mut conn = self.client.get_multiplexed_async_connection().await?;\n        let result: Option&lt;Vec&lt;u8&gt;&gt; = redis::cmd(\"GET\")\n            .arg(key)\n            .query_async(&amp;mut conn)\n            .await?;\n        Ok(result)\n    }\n\n    async fn set(&amp;self, key: &amp;str, value: &amp;[u8], ttl_secs: u64) -&gt; anyhow::Result&lt;()&gt; {\n        let mut conn = self.client.get_multiplexed_async_connection().await?;\n        redis::cmd(\"SETEX\")\n            .arg(key)\n            .arg(ttl_secs)\n            .arg(value)\n            .query_async(&amp;mut conn)\n            .await?;\n        Ok(())\n    }\n\n    async fn invalidate(&amp;self, key: &amp;str) -&gt; anyhow::Result&lt;()&gt; {\n        let mut conn = self.client.get_multiplexed_async_connection().await?;\n        redis::cmd(\"DEL\").arg(key).query_async(&amp;mut conn).await?;\n        Ok(())\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Test Implementations\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::collections::HashMap;\n    use tokio::sync::RwLock;\n\n    struct MockUserRepository {\n        users: RwLock&lt;HashMap&lt;i64, User&gt;&gt;,\n    }\n\n    impl MockUserRepository {\n        fn new() -&gt; Self {\n            Self {\n                users: RwLock::new(HashMap::new()),\n            }\n        }\n\n        fn with_users(users: Vec&lt;User&gt;) -&gt; Self {\n            let map: HashMap&lt;i64, User&gt; = users\n                .into_iter()\n                .map(|u| (u.id, u))\n                .collect();\n            Self {\n                users: RwLock::new(map),\n            }\n        }\n    }\n\n    #[async_trait]\n    impl UserRepository for MockUserRepository {\n        async fn find_by_id(&amp;self, id: i64) -&gt; anyhow::Result&lt;Option&lt;User&gt;&gt; {\n            Ok(self.users.read().await.get(&amp;id).cloned())\n        }\n\n        async fn save(&amp;self, user: &amp;User) -&gt; anyhow::Result&lt;()&gt; {\n            self.users.write().await.insert(user.id, user.clone());\n            Ok(())\n        }\n\n        async fn delete(&amp;self, id: i64) -&gt; anyhow::Result&lt;()&gt; {\n            self.users.write().await.remove(&amp;id);\n            Ok(())\n        }\n    }\n\n    struct MockCacheStore {\n        data: RwLock&lt;HashMap&lt;String, Vec&lt;u8&gt;&gt;&gt;,\n    }\n\n    impl MockCacheStore {\n        fn new() -&gt; Self {\n            Self {\n                data: RwLock::new(HashMap::new()),\n            }\n        }\n    }\n\n    #[async_trait]\n    impl CacheStore for MockCacheStore {\n        async fn get(&amp;self, key: &amp;str) -&gt; anyhow::Result&lt;Option&lt;Vec&lt;u8&gt;&gt;&gt; {\n            Ok(self.data.read().await.get(key).cloned())\n        }\n\n        async fn set(&amp;self, key: &amp;str, value: &amp;[u8], _ttl: u64) -&gt; anyhow::Result&lt;()&gt; {\n            self.data.write().await.insert(key.to_string(), value.to_vec());\n            Ok(())\n        }\n\n        async fn invalidate(&amp;self, key: &amp;str) -&gt; anyhow::Result&lt;()&gt; {\n            self.data.write().await.remove(key);\n            Ok(())\n        }\n    }\n\n    #[tokio::test]\n    async fn test_get_user_cache_miss() {\n        let user = User { id: 1, name: \"Alice\".into(), email: \"alice@test.com\".into() };\n        let repo = Arc::new(MockUserRepository::with_users(vec![user.clone()]));\n        let cache = Arc::new(MockCacheStore::new());\n        let service = UserService::new(repo, cache.clone());\n\n        let result = service.get_user(1).await.unwrap();\n        assert_eq!(result, Some(user));\n\n        \u002F\u002F Verify it was cached\n        assert!(cache.data.read().await.contains_key(\"user:1\"));\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"pattern-2-generics-zero-cost-abstraction\">Pattern 2: Generics (Zero-Cost Abstraction)\u003C\u002Fh2>\n\u003Cp>For performance-critical paths where dynamic dispatch overhead matters, use generics:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct UserService&lt;R: UserRepository, C: CacheStore&gt; {\n    repo: R,\n    cache: C,\n}\n\nimpl&lt;R: UserRepository, C: CacheStore&gt; UserService&lt;R, C&gt; {\n    fn new(repo: R, cache: C) -&gt; Self {\n        Self { repo, cache }\n    }\n\n    async fn get_user(&amp;self, id: i64) -&gt; anyhow::Result&lt;Option&lt;User&gt;&gt; {\n        \u002F\u002F Same logic, but statically dispatched\n        \u002F\u002F Compiler monomorphizes for each (R, C) combination\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Trade-offs:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Aspect\u003C\u002Fth>\u003Cth>Arc\u003Cdyn Trait>\u003C\u002Fth>\u003Cth>Generics\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Runtime cost\u003C\u002Ftd>\u003Ctd>vtable lookup (~1ns)\u003C\u002Ftd>\u003Ctd>Zero\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Binary size\u003C\u002Ftd>\u003Ctd>Smaller\u003C\u002Ftd>\u003Ctd>Larger (monomorphization)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Flexibility\u003C\u002Ftd>\u003Ctd>Can swap at runtime\u003C\u002Ftd>\u003Ctd>Fixed at compile time\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Compile time\u003C\u002Ftd>\u003Ctd>Faster\u003C\u002Ftd>\u003Ctd>Slower\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Ergonomics\u003C\u002Ftd>\u003Ctd>Simpler signatures\u003C\u002Ftd>\u003Ctd>Generic bounds propagate\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>For most applications, \u003Ccode>Arc&lt;dyn Trait&gt;\u003C\u002Fcode> is the right choice. The vtable lookup cost is negligible compared to the database or network calls your service makes. Use generics only in hot loops where nanoseconds matter.\u003C\u002Fp>\n\u003Ch2 id=\"the-composition-root-pattern\">The Composition Root Pattern\u003C\u002Fh2>\n\u003Cp>Wire everything together at the application entry point:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">#[tokio::main]\nasync fn main() -&gt; anyhow::Result&lt;()&gt; {\n    \u002F\u002F Initialize infrastructure\n    let db_pool = PgPool::connect(&amp;std::env::var(\"DATABASE_URL\")?).await?;\n    let redis = redis::Client::open(std::env::var(\"REDIS_URL\")?)?;\n\n    \u002F\u002F Build repositories\n    let user_repo: Arc&lt;dyn UserRepository&gt; = Arc::new(\n        PostgresUserRepository { pool: db_pool.clone() }\n    );\n    let cache: Arc&lt;dyn CacheStore&gt; = Arc::new(\n        RedisCacheStore { client: redis }\n    );\n\n    \u002F\u002F Build services\n    let user_service = Arc::new(UserService::new(\n        user_repo.clone(),\n        cache.clone(),\n    ));\n\n    \u002F\u002F Build app state\n    let state = AppState {\n        user_service,\n        \u002F\u002F ... other services\n    };\n\n    \u002F\u002F Start server\n    let app = Router::new()\n        .route(\"\u002Fusers\u002F:id\", get(get_user_handler))\n        .with_state(state);\n\n    let listener = tokio::net::TcpListener::bind(\"0.0.0.0:3001\").await?;\n    axum::serve(listener, app).await?;\n    Ok(())\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>All dependencies are created in \u003Ccode>main()\u003C\u002Fcode> and passed downward. No global state, no service locators called at arbitrary points. This is the purest form of DI.\u003C\u002Fp>\n\u003Ch2 id=\"the-servicelocator-pattern\">The ServiceLocator Pattern\u003C\u002Fh2>\n\u003Cp>For complex systems with dozens of services, passing every dependency through constructors becomes unwieldy. The ServiceLocator pattern provides a registry that components can query:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::any::{Any, TypeId};\nuse std::collections::HashMap;\nuse std::sync::Arc;\n\n#[derive(Clone, Default)]\nstruct ServiceLocator {\n    services: Arc&lt;HashMap&lt;TypeId, Box&lt;dyn Any + Send + Sync&gt;&gt;&gt;,\n}\n\nimpl ServiceLocator {\n    fn builder() -&gt; ServiceLocatorBuilder {\n        ServiceLocatorBuilder {\n            services: HashMap::new(),\n        }\n    }\n\n    fn get&lt;T: 'static + Send + Sync&gt;(&amp;self) -&gt; Option&lt;&amp;T&gt; {\n        self.services\n            .get(&amp;TypeId::of::&lt;T&gt;())\n            .and_then(|boxed| boxed.downcast_ref::&lt;T&gt;())\n    }\n\n    fn resolve&lt;T: 'static + Send + Sync&gt;(&amp;self) -&gt; &amp;T {\n        self.get::&lt;T&gt;()\n            .unwrap_or_else(|| panic!(\n                \"Service not registered: {}\",\n                std::any::type_name::&lt;T&gt;()\n            ))\n    }\n}\n\nstruct ServiceLocatorBuilder {\n    services: HashMap&lt;TypeId, Box&lt;dyn Any + Send + Sync&gt;&gt;,\n}\n\nimpl ServiceLocatorBuilder {\n    fn register&lt;T: 'static + Send + Sync&gt;(mut self, service: T) -&gt; Self {\n        self.services.insert(TypeId::of::&lt;T&gt;(), Box::new(service));\n        self\n    }\n\n    fn build(self) -&gt; ServiceLocator {\n        ServiceLocator {\n            services: Arc::new(self.services),\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Usage:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">let locator = ServiceLocator::builder()\n    .register::&lt;Arc&lt;dyn UserRepository&gt;&gt;(Arc::new(pg_repo))\n    .register::&lt;Arc&lt;dyn CacheStore&gt;&gt;(Arc::new(redis_cache))\n    .register::&lt;Arc&lt;UserService&gt;&gt;(Arc::new(user_service))\n    .build();\n\n\u002F\u002F Later, in a handler:\nlet user_service = locator.resolve::&lt;Arc&lt;UserService&gt;&gt;();\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The ServiceLocator trades compile-time safety for runtime flexibility. It is a controlled anti-pattern: use it at the composition root to simplify wiring, but prefer constructor injection within individual services.\u003C\u002Fp>\n\u003Ch2 id=\"conclusion\">Conclusion\u003C\u002Fh2>\n\u003Cp>Dependency injection in Rust does not require a framework. Traits define contracts, \u003Ccode>Arc&lt;dyn Trait&gt;\u003C\u002Fcode> provides runtime polymorphism, and the composition root wires everything together. Use mock implementations for testing, generics for hot paths, and the ServiceLocator pattern only when constructor chains become unmanageable. The key principle is the same across all languages: depend on abstractions, not concretions.\u003C\u002Fp>\n","en","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.149585Z","Dependency Injection in Rust — ServiceLocator, Arc, and Trait Objects","Implement dependency injection in Rust without frameworks using Arc, trait objects, the composition root pattern, and ServiceLocator for complex systems.","dependency injection rust",null,"index, follow",[22,27],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000022","Performance","performance","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000001","Rust","rust","Engineering",[33,39,45],{"id":34,"title":35,"slug":36,"excerpt":37,"locale":12,"category_name":31,"published_at":38},"d0200000-0000-0000-0000-000000000003","Why Bali Is Becoming Southeast Asia's Impact-Tech Hub in 2026","why-bali-becoming-southeast-asia-impact-tech-hub-2026","Bali ranks #16 among Southeast Asian startup ecosystems. With a growing concentration of Web3 builders, AI sustainability startups, and eco-travel tech companies, the island is carving a niche as the region's impact-tech capital.","2026-03-28T10:44:37.748283Z",{"id":40,"title":41,"slug":42,"excerpt":43,"locale":12,"category_name":31,"published_at":44},"d0200000-0000-0000-0000-000000000002","ASEAN Data Protection Patchwork: A Developer's Compliance Checklist","asean-data-protection-patchwork-developer-compliance-checklist","Seven ASEAN countries now have comprehensive data protection laws, each with different consent models, localization requirements, and penalty structures. Here is a practical compliance checklist for developers building multi-country applications.","2026-03-28T10:44:37.374741Z",{"id":46,"title":47,"slug":48,"excerpt":49,"locale":12,"category_name":31,"published_at":50},"d0200000-0000-0000-0000-000000000001","Indonesia's $29 Billion Digital Transformation: Opportunities for Software Companies","indonesia-29-billion-digital-transformation-opportunities-software-companies","Indonesia's IT services market is projected to reach $29.03 billion in 2026, up from $24.37 billion in 2025. Cloud infrastructure, AI, e-commerce, and data centers are driving the fastest growth in Southeast Asia.","2026-03-28T10:44:37.349311Z",{"id":13,"name":52,"slug":53,"bio":54,"photo_url":19,"linkedin":19,"role":55,"created_at":56,"updated_at":56},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]