Chapter 4: Dependency Injection with Context Pattern
Introduction
Rust’s ownership system makes dependency injection (DI) different from languages with garbage collection. You can’t just pass references everywhere - you need to think about lifetimes and ownership.
Pierre uses Arc
Key concepts:
- Dependency Injection: Providing dependencies to a struct rather than creating them internally
- Arc
: Thread-safe reference-counted smart pointer - Service Locator: Anti-pattern where a single struct holds all dependencies
- Focused Contexts: Better pattern with separate contexts for different domains
The Problem: Expensive Resource Creation
Consider what happens without dependency injection:
#![allow(unused)]
fn main() {
// ANTI-PATTERN: Creating expensive resources repeatedly
async fn handle_request(user_id: &str) -> Result<Response> {
// Creates new database connection (expensive!)
let database = Database::new(&config.database_url).await?;
// Creates new auth manager (unnecessary!)
let auth_manager = AuthManager::new(24);
// Use them...
let user = database.get_user(user_id).await?;
let token = auth_manager.create_token(&user)?;
Ok(response)
}
}
Problems:
- Performance: Database connection pool created per request
- Resource exhaustion: Each connection uses memory/file descriptors
- Configuration duplication: Same config loaded repeatedly
- No sharing: Can’t share state (caches, metrics) between requests
Solution 1: Dependency Injection with Arc<T>
Arc (Atomic Reference Counting) enables shared ownership across threads.
Arc Basics
#![allow(unused)]
fn main() {
use std::sync::Arc;
// Create an expensive resource once
let database = Arc::new(Database::new(&config).await?);
// Clone the Arc (cheap - just increments counter)
let db_clone = Arc::clone(&database); // Or database.clone()
// Both point to the same underlying Database
// When last Arc is dropped, Database is dropped
}
Rust Idioms Explained:
-
Arc::new(value)- Wrap value in atomic reference counter- Allocates on heap
- Returns
Arc<T> - Thread-safe (uses atomic operations)
-
Arc::clone(&arc)vs.clone()- Both do the same thing (increment counter)
Arc::clonemakes it explicit (recommended in docs).clone()is shorter (common in Pierre)
-
Drop semantics
- Each
Arc::clone()increments counter - Each drop decrements counter
- When counter reaches 0, inner value is dropped
- Each
-
Cost
- Creating Arc: One heap allocation
- Cloning Arc: Increment atomic counter (~1-2 CPU instructions)
- Accessing data: No overhead (just deref)
Reference: Rust Book - Arc
Dependency Injection Example
use std::sync::Arc;
// 1. Create expensive resources once at startup
#[tokio::main]
async fn main() -> Result<()> {
let database = Arc::new(Database::new(&config).await?);
let auth_manager = Arc::new(AuthManager::new(24));
// 2. Pass to HTTP handlers via Axum state
let app = Router::new()
.route("/users/:id", get(get_user_handler))
.with_state(AppState { database, auth_manager });
// 3. Listen for requests
axum::Server::bind(&addr).serve(app.into_make_service()).await?;
Ok(())
}
// Handler receives dependencies via State extractor
async fn get_user_handler(
State(state): State<AppState>,
Path(user_id): Path<String>,
) -> Result<Json<User>, AppError> {
// database and auth_manager are Arc clones (cheap)
let user = state.database.get_user(&user_id).await?;
let token = state.auth_manager.create_token(&user)?;
Ok(Json(user))
}
#[derive(Clone)]
struct AppState {
database: Arc<Database>,
auth_manager: Arc<AuthManager>,
}
Pattern:
- Create once → Wrap in Arc → Share via cloning Arc
Reference: Axum - Sharing State
Serverresources: Centralized Dependency Container
Pierre uses ServerResources as a central container for all dependencies.
Source: src/mcp/resources.rs:35-77
#![allow(unused)]
fn main() {
/// Centralized resource container for dependency injection
#[derive(Clone)]
pub struct ServerResources {
/// Database connection pool for persistent storage operations
pub database: Arc<Database>,
/// Authentication manager for user identity verification
pub auth_manager: Arc<AuthManager>,
/// JSON Web Key Set manager for RS256 JWT signing and verification
pub jwks_manager: Arc<JwksManager>,
/// Authentication middleware for MCP request validation
pub auth_middleware: Arc<McpAuthMiddleware>,
/// WebSocket connection manager for real-time updates
pub websocket_manager: Arc<WebSocketManager>,
/// Server-Sent Events manager for streaming notifications
pub sse_manager: Arc<crate::sse::SseManager>,
/// OAuth client for multi-tenant authentication flows
pub tenant_oauth_client: Arc<TenantOAuthClient>,
/// Registry of fitness data providers (Strava, Fitbit, Garmin, WHOOP, Terra)
pub provider_registry: Arc<ProviderRegistry>,
/// Secret key for admin JWT token generation
pub admin_jwt_secret: Arc<str>,
/// Server configuration loaded from environment
pub config: Arc<crate::config::environment::ServerConfig>,
/// AI-powered fitness activity analysis engine
pub activity_intelligence: Arc<ActivityIntelligence>,
/// A2A protocol client manager
pub a2a_client_manager: Arc<A2AClientManager>,
/// Service for managing A2A system user accounts
pub a2a_system_user_service: Arc<A2ASystemUserService>,
/// Broadcast channel for OAuth completion notifications
pub oauth_notification_sender: Option<broadcast::Sender<OAuthCompletedNotification>>,
/// Cache layer for performance optimization
pub cache: Arc<Cache>,
/// Optional plugin executor for custom tool implementations
pub plugin_executor: Option<Arc<PluginToolExecutor>>,
/// Configuration for PII redaction in logs and responses
pub redaction_config: Arc<RedactionConfig>,
/// Rate limiter for OAuth2 endpoints
pub oauth2_rate_limiter: Arc<crate::oauth2_server::rate_limiting::OAuth2RateLimiter>,
}
}
Rust Idioms Explained:
-
#[derive(Clone)]on struct withArcfields- Cloning
ServerResourcesclones all theArcs (cheap) - Does NOT clone underlying data (Database, AuthManager, etc.)
- Enables passing resources around without lifetime parameters
- Cloning
-
Arc<str>for string secrets- More memory efficient than
Arc<String> - Immutable (strings never change)
- Implements
AsRef<str>for easy access
- More memory efficient than
-
Option<Arc<T>>for optional dependenciesplugin_executormay not be initializedNonemeans feature disabledSome(Arc<...>)when enabled
Creating Serverresources
Source: src/mcp/resources.rs:85-150
#![allow(unused)]
fn main() {
impl ServerResources {
pub fn new(
database: Database,
auth_manager: AuthManager,
admin_jwt_secret: &str,
config: Arc<crate::config::environment::ServerConfig>,
cache: Cache,
rsa_key_size_bits: usize,
jwks_manager: Option<Arc<JwksManager>>,
) -> Self {
// Wrap expensive resources in Arc once
let database_arc = Arc::new(database);
let auth_manager_arc = Arc::new(auth_manager);
// Create dependent resources
let tenant_oauth_client = Arc::new(TenantOAuthClient::new(
TenantOAuthManager::new(Arc::new(config.oauth.clone()))
));
let provider_registry = Arc::new(ProviderRegistry::new());
// Create intelligence engine
let activity_intelligence = Self::create_default_intelligence();
// Create A2A components
let a2a_system_user_service = Arc::new(
A2ASystemUserService::new(database_arc.clone())
);
let a2a_client_manager = Arc::new(A2AClientManager::new(
database_arc.clone(),
a2a_system_user_service.clone(),
));
// Wrap cache
let cache_arc = Arc::new(cache);
// Load or create JWKS manager
let jwks_manager_arc = jwks_manager.unwrap_or_else(|| {
// Load from database or create new
// ... (initialization logic)
Arc::new(new_jwks)
});
Self {
database: database_arc,
auth_manager: auth_manager_arc,
jwks_manager: jwks_manager_arc,
tenant_oauth_client,
provider_registry,
// ... all other fields
}
}
}
}
Pattern observations:
-
Accept owned values (
database: Database)- Not
Arc<Database>in parameters - Caller doesn’t need to know about Arc
new()wraps in Arc internally
- Not
-
Return
Self(notArc<Self>)- Caller decides if they need Arc
- Typical usage:
Arc::new(ServerResources::new(...))
-
.clone()on Arc is explicit- Shows resource sharing happening
- Comments explain why (see line 9 note about “Safe” clones)
Using Serverresources
Source: src/bin/pierre-mcp-server.rs:182-220
#![allow(unused)]
fn main() {
fn create_server(
database: Database,
auth_manager: AuthManager,
jwt_secret: &str,
config: &ServerConfig,
cache: Cache,
) -> MultiTenantMcpServer {
let rsa_key_size = get_rsa_key_size();
// Create resources (wraps everything in Arc)
let mut resources_instance = ServerResources::new(
database,
auth_manager,
jwt_secret,
Arc::new(config.clone()),
cache,
rsa_key_size,
None, // Generate new JWKS
);
// Wrap in Arc for sharing
let resources_arc = Arc::new(resources_instance.clone());
// Initialize plugin system (needs Arc<ServerResources>)
let plugin_executor = PluginToolExecutor::new(resources_arc);
// Set plugin executor back on resources
resources_instance.set_plugin_executor(Arc::new(plugin_executor));
// Final Arc wrapping
let resources = Arc::new(resources_instance);
// Create server with resources
MultiTenantMcpServer::new(resources)
}
}
Pattern: Create → Arc wrap → Share → Modify → Re-wrap
The Service Locator Anti-Pattern
While ServerResources works, it’s a service locator anti-pattern.
Problems with service locator:
- God object - Single struct knows about everything
- Hidden dependencies - Functions take
ServerResourcesbut only use 1-2 fields - Testing complexity - Must mock entire
ServerResourceseven for simple tests - Tight coupling - Adding new dependency requires changing one big struct
- Unclear requirements - Can’t tell from signature what function needs
Example of the problem:
#![allow(unused)]
fn main() {
// What does this function actually need?
async fn process_activity(
resources: &ServerResources,
activity_id: &str,
) -> Result<ProcessedActivity> {
// Uses only database and intelligence
let activity = resources.database.get_activity(activity_id).await?;
let analysis = resources.activity_intelligence.analyze(&activity)?;
Ok(analysis)
}
// Better: explicit dependencies
async fn process_activity(
database: &Database,
intelligence: &ActivityIntelligence,
activity_id: &str,
) -> Result<ProcessedActivity> {
// Clear what's needed!
let activity = database.get_activity(activity_id).await?;
let analysis = intelligence.analyze(&activity)?;
Ok(analysis)
}
}
Reference: Service Locator Anti-Pattern
Solution 2: Focused Context Pattern
Pierre is evolving toward focused contexts that group related dependencies.
Source: src/context/mod.rs:1-40
#![allow(unused)]
fn main() {
//! Focused dependency injection contexts
//!
//! This module replaces the `ServerResources` service locator anti-pattern with
//! focused contexts that provide only the dependencies needed for specific operations.
//!
//! # Architecture
//!
//! - `AuthContext`: Authentication and authorization dependencies
//! - `DataContext`: Database and data provider dependencies
//! - `ConfigContext`: Configuration and OAuth management dependencies
//! - `NotificationContext`: WebSocket and SSE notification dependencies
/// Authentication context
pub mod auth;
/// Configuration context
pub mod config;
/// Data context
pub mod data;
/// Notification context
pub mod notification;
/// Server context combining all focused contexts
pub mod server;
// Re-exports
pub use auth::AuthContext;
pub use config::ConfigContext;
pub use data::DataContext;
pub use notification::NotificationContext;
pub use server::ServerContext;
}
Focused Context Example
#![allow(unused)]
fn main() {
// Conceptual example of focused contexts
/// Context for authentication operations
#[derive(Clone)]
pub struct AuthContext {
pub auth_manager: Arc<AuthManager>,
pub jwks_manager: Arc<JwksManager>,
pub middleware: Arc<McpAuthMiddleware>,
}
/// Context for data operations
#[derive(Clone)]
pub struct DataContext {
pub database: Arc<Database>,
pub provider_registry: Arc<ProviderRegistry>,
pub cache: Arc<Cache>,
}
/// Context for configuration operations
#[derive(Clone)]
pub struct ConfigContext {
pub config: Arc<ServerConfig>,
pub tenant_oauth_client: Arc<TenantOAuthClient>,
}
// Use specific contexts
async fn authenticate_user(
auth_ctx: &AuthContext,
token: &str,
) -> Result<User> {
// Only has access to auth-related dependencies
auth_ctx.auth_manager.validate_token(token)
}
async fn fetch_activities(
data_ctx: &DataContext,
user_id: &str,
) -> Result<Vec<Activity>> {
// Only has access to data-related dependencies
data_ctx.database.get_activities(user_id).await
}
}
Benefits:
- ✅ Clear dependencies - Function signature shows what it needs
- ✅ Easier testing - Mock only relevant context
- ✅ Better organization - Related dependencies grouped
- ✅ Loose coupling - Changes to one context don’t affect others
- ✅ Type safety - Compiler prevents using wrong context
Arc<T> vs Rc<T> vs Box<T>
Understanding when to use each smart pointer:
| Type | Thread-Safe? | Overhead | Use When |
|---|---|---|---|
Box<T> | N/A | Single allocation | Single ownership, heap allocation |
Rc<T> | ❌ No | Non-atomic counter | Shared ownership, single thread |
Arc<T> | ✅ Yes | Atomic counter | Shared ownership, multi-threaded |
Pierre uses Arc<T> because:
- Axum handlers run on different threads
- Need to share resources across concurrent requests
- Thread safety is non-negotiable in async runtime
When to use each:
#![allow(unused)]
fn main() {
// Box<T> - Single ownership
let config = Box::new(Config::from_file("config.toml")?);
drop(config); // Config is dropped
// Rc<T> - Shared ownership, single thread
use std::rc::Rc;
let data = Rc::new(vec![1, 2, 3]);
let data2 = Rc::clone(&data);
// Both point to same Vec, single-threaded only
// Arc<T> - Shared ownership, multi-threaded
use std::sync::Arc;
let database = Arc::new(Database::new()?);
tokio::spawn(async move {
database.query(...).await // Can use in another thread
});
}
Reference: Rust Book - Smart Pointers
Interior Mutability with Arc<Mutex<T>>
Arc provides shared ownership, but data is immutable. For mutable shared state, use Mutex.
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
// Shared mutable counter
let counter = Arc::new(Mutex::new(0));
// Spawn multiple tasks that increment counter
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
tokio::spawn(async move {
let mut num = counter_clone.lock().unwrap(); // Acquire lock
*num += 1;
}); // Lock automatically released when `num` is dropped
}
}
Rust Idioms Explained:
-
Arc<Mutex<T>>patternArcfor shared ownershipMutexfor exclusive access- Common pattern for shared mutable state
-
.lock()returnsMutexGuard- RAII guard that unlocks on drop
- Implements
DerefandDerefMut - Access inner value with
*guard
-
When to use:
- ✅ Occasional writes (metrics, caches)
- ❌ Frequent writes (use channels/actors instead)
- ❌ Async code (use
tokio::sync::Mutexinstead)
Pierre examples:
WebSocketManagerusesDashMap(concurrent HashMap)CacheusesMutexfor LRU eviction- Most resources are immutable after creation
Reference: Rust Book - Mutex
Diagram: Dependency Injection Flow
┌──────────────────────────────────────────────────────────┐
│ Application Startup │
└──────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Create Expensive Resources Once │
│ - Database (connection pool) │
│ - AuthManager (key material) │
│ - JwksManager (RSA keys) │
│ - Cache (LRU storage) │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Wrap in Arc<T> │
│ - Arc::new(database) │
│ - Arc::new(auth_manager) │
│ - Arc::new(jwks_manager) │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Create ServerResources │
│ (or focused contexts) │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Wrap ServerResources in Arc │
│ Arc::new(resources) │
└─────────────────┬───────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│Handler 1 │ │Handler 2 │ │Handler N │
│resources │ │resources │ │resources │
│.clone() │ │.clone() │ │.clone() │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└────────────────┼────────────────┘
│
▼
┌──────────────────────────────────┐
│ All point to same resources │
│ (Arc counter = N) │
│ Memory allocated once │
└──────────────────────────────────┘
Rust Idioms Summary
| Idiom | Purpose | Example Location |
|---|---|---|
Arc<T> | Shared ownership across threads | src/mcp/resources.rs:40-77 |
Arc::clone() | Increment reference count | src/mcp/resources.rs:98-113 |
#[derive(Clone)] on Arc struct | Cheap struct cloning | src/mcp/resources.rs:39 |
Arc<str> | Efficient immutable string sharing | src/mcp/resources.rs:58 |
Option<Arc<T>> | Optional shared dependencies | src/mcp/resources.rs:72 |
| Focused contexts | Domain-specific DI containers | src/context/mod.rs |
References:
Key Takeaways
- Arc
enables shared ownership - Thread-safe reference counting - Cloning Arc is cheap - Just increments atomic counter
- Create once, share everywhere - Wrap expensive resources in Arc at startup
- Service locator is an anti-pattern - Use focused contexts instead
- Explicit dependencies - Function signatures should show what’s needed
- Arc vs Rc vs Box - Choose based on threading and ownership needs
- Interior mutability - Use
MutexorRwLockfor mutable shared state
Next Chapter
Chapter 5: Cryptographic Key Management - Learn Pierre’s two-tier key management system (MEK + DEK), RSA key generation for JWT signing, and the zeroize crate for secure memory cleanup.