Skip to content

Latest commit

 

History

History
231 lines (165 loc) · 6.64 KB

File metadata and controls

231 lines (165 loc) · 6.64 KB

Decision: ExecutionContext Wrapper Pattern

Status

Accepted

Date

2025-11-07

Context

During the implementation of the Dispatch Layer (Proposal 2 from the presentation layer reorganization epic #154), we needed to decide how command handlers should access application services. We had two main options:

  1. Direct Container Access: Pass the Container directly to command handlers
  2. ExecutionContext Wrapper: Create an ExecutionContext wrapper around the Container

Current Architecture

The Dispatch Layer routes commands to handlers and needs to provide access to application services (user output, repositories, external tool clients, etc.). These services are managed by the dependency injection Container.

Design Options Considered

Option 1: Direct Container Access

pub fn route_command(
    command: Commands,
    working_dir: &Path,
    container: &Container,
) -> Result<(), CommandError>

// In handlers:
fn handle_create_command(container: &Container) {
    let user_output = container.user_output();
    // ...
}

Option 2: ExecutionContext Wrapper

pub struct ExecutionContext {
    container: Arc<Container>,
}

pub fn route_command(
    command: Commands,
    working_dir: &Path,
    context: &ExecutionContext,
) -> Result<(), CommandError>

// In handlers:
fn handle_create_command(context: &ExecutionContext) {
    let user_output = context.user_output();
    // ...
}

Decision

We chose Option 2: ExecutionContext Wrapper for the following reasons:

1. Future-Proof Command Signatures

By introducing ExecutionContext, we can add execution-related data in the future without breaking existing command handler signatures:

pub struct ExecutionContext {
    container: Arc<Container>,
    // Future additions without breaking changes:
    // request_id: RequestId,
    // execution_metadata: ExecutionMetadata,
    // tracing_context: TracingContext,
    // user_permissions: UserPermissions,
    // execution_timeout: Duration,
}

If we used Container directly, adding any execution context would require changing every command handler signature.

2. Clear Abstraction and Intent

ExecutionContext provides a logical abstraction for "everything a command needs to execute" rather than exposing the dependency injection container directly:

  • Container: Implementation detail for dependency injection
  • ExecutionContext: Execution abstraction for command handlers

This makes the intent clearer and separates concerns properly.

3. Type Safety and Interface Clarity

// Less clear: What is this container for? Bootstrapping? Testing? Execution?
fn handle_command(container: &Container)

// Clear: This is specifically for command execution
fn handle_command(context: &ExecutionContext)

4. Command-Specific Service Aggregation

ExecutionContext can provide command-specific convenience methods and service aggregations:

impl ExecutionContext {
    // Direct service access
    pub fn user_output(&self) -> &Arc<Mutex<UserOutput>> {
        self.container.user_output()
    }

    // Future: Command-specific aggregated services
    pub fn deployment_services(&self) -> DeploymentServices {
        DeploymentServices {
            provisioner: self.container.provisioner(),
            configurator: self.container.configurator(),
            validator: self.container.validator(),
        }
    }
}

5. Enhanced Testability

Different execution contexts can be created for different scenarios:

// Production context
let context = ExecutionContext::new(container);

// Test context with mocks
let context = TestExecutionContext::new(mock_container);

// Both can implement the same interface
trait ExecutionContextTrait {
    fn user_output(&self) -> &Arc<Mutex<UserOutput>>;
}

6. Industry Pattern Alignment

Most frameworks use execution context patterns:

  • Spring Framework: ApplicationContext
  • ASP.NET Core: HttpContext
  • Express.js: Request/Response context
  • Go: context.Context

This aligns with established patterns for managing execution state.

Consequences

Positive

  • Future-Proof: Can extend execution context without breaking command signatures
  • Clear Intent: ExecutionContext clearly indicates its purpose for command execution
  • Better Abstraction: Separates execution concerns from dependency injection mechanics
  • Enhanced Testability: Enables different contexts for different testing scenarios
  • Industry Alignment: Follows established patterns from major frameworks

Negative

  • Initial Overhead: Currently just a thin wrapper around Container
  • Additional Indirection: One extra layer between commands and services
  • Learning Curve: New developers need to understand the wrapper pattern

Migration Path

If needed, migration from Container to ExecutionContext (or vice versa) is straightforward:

// From Container to ExecutionContext
fn handle_command(container: &Container) -> fn handle_command(context: &ExecutionContext)

// From ExecutionContext to Container
fn handle_command(context: &ExecutionContext) -> fn handle_command(container: &Container)

Implementation Details

Current Implementation

pub struct ExecutionContext {
    container: Arc<Container>,
}

impl ExecutionContext {
    pub fn new(container: Arc<Container>) -> Self {
        Self { container }
    }

    pub fn container(&self) -> &Container {
        &self.container
    }

    pub fn user_output(&self) -> &Arc<Mutex<UserOutput>> {
        self.container.user_output()
    }
}

Usage Pattern

// In bootstrap/app.rs
let container = Arc::new(Container::new());
let context = ExecutionContext::new(container);

// In dispatch layer
route_command(command, working_dir, &context)?;

// In command handlers
fn handle_create_command(context: &ExecutionContext) {
    let user_output = context.user_output();
    // Command implementation
}

Related Decisions

References