torrust-tracker-deployer 0.1.0

Torrust Tracker Deployer - Deployment Infrastructure with Ansible and OpenTofu
Documentation
# Decision: Command State Return Pattern

## Status

Accepted

## Date

2025-10-03

## Context

In Phase 5 of the environment state management feature, we need to integrate type-safe state transitions into our command handlers (`ProvisionCommand` and `ConfigureCommand`). This raises a fundamental architectural question: **Should commands return the transformed `Environment<S>` state, or should they operate as pure command handlers that only persist state via the repository?**

### The Problem

We have two competing patterns:

1. **Typed State Returns**: Commands accept `Environment<S>` and return `Environment<T>`

   - Example: `ProvisionCommand::execute(Environment<Created>) -> Result<Environment<Provisioned>, Error>`

2. **Pure Command Handler**: Commands accept `Environment<S>`, persist state internally, return void
   - Example: `ProvisionCommand::execute(Environment<Created>) -> Result<(), Error>`

Both patterns can work, but they have different implications for type safety, data flow, and future extensibility.

### Why This Matters

- We've invested 4 phases of work building a sophisticated type-state pattern for compile-time state validation
- Commands orchestrate complex multi-step workflows with clear state progressions
- We want to enable future command chaining and orchestration with compile-time guarantees
- The repository layer exists for persistence, not as the primary data flow mechanism

## Decision

**We will use typed state returns**: Commands accept and return strongly-typed `Environment<S>` states.

```rust
impl ProvisionCommand {
    pub async fn execute(
        &self,
        environment: Environment<Created>,
    ) -> Result<Environment<Provisioned>, ProvisionCommandError> {
        // Transition to intermediate state
        let environment = environment.start_provisioning();
        self.persist_state(&environment)?; // Persistence is secondary

        // Execute provisioning steps...
        let provisioned = self.execute_steps(&environment).await?;

        // Persist final state
        self.persist_state(&provisioned)?;

        // Return transformed state
        Ok(provisioned)
    }
}
```

### Key Principles

1. **Commands are state transformations**: `Environment<S>` → `Environment<T>`
2. **Repository is for persistence**: Save/load state, but not primary data flow
3. **Type safety is paramount**: Leverage compile-time guarantees from type-state pattern
4. **Data flow is explicit**: Input → transform → output (no hidden state)

## Consequences

### Positive

✅ **Compile-Time Safety**: Invalid state transitions are prevented at compile time

- Cannot call `ConfigureCommand` on an `Environment<Created>` (not yet provisioned)
- Cannot call `ProvisionCommand` on an already `Environment<Provisioned>` instance
- Impossible to forget a state transition

✅ **Clear Data Flow**: Easy to understand what's happening

```rust
let created = Environment::new(...);
let provisioned = provision_command.execute(created).await?;
let configured = configure_command.execute(provisioned).await?;
```

✅ **No Repeated Parsing**: Avoid pattern matching on `AnyEnvironmentState`

- Without typed returns: Load from repository → match on `AnyEnvironmentState` → extract typed state
- With typed returns: State already typed, no parsing needed

✅ **Future Orchestration**: Enables fluent command chaining

```rust
// Future possibility:
let workflow = Workflow::new()
    .then(provision_command)
    .then(configure_command)
    .then(deploy_command);

let final_state = workflow.execute(created).await?;
```

✅ **Type-State Pattern Reaches Full Potential**: Commands leverage all the work from Phases 1-4

### Negative

⚠️ **Deviates from Pure CQS**: Commands traditionally shouldn't return values in strict Command/Query Separation

- However, CQRS patterns allow commands to return acknowledgments/identifiers
- State transformation is a valid command output in functional paradigms

⚠️ **Commands Return Values**: Not traditional "fire and forget" command handlers

- However, this is intentional - we want the transformed state for chaining

### Neutral

ℹ️ **Repository is Secondary**: State persistence happens alongside transformation

- This is by design - persistence is a cross-cutting concern, not the primary data flow
- Failed persistence is logged but doesn't fail the command (state is still valid in memory)

## Alternatives Considered

### Alternative 1: Pure Command Handler Pattern

```rust
impl ProvisionCommand {
    pub async fn execute(
        &self,
        environment: Environment<Created>,
    ) -> Result<(), ProvisionCommandError> {
        let environment = environment.start_provisioning();
        self.repository.save(&environment.into_any())?;

        // Execute steps...

        let provisioned = environment.complete_provisioning(ip);
        self.repository.save(&provisioned.into_any())?;

        // No return - caller must load from repository
    }
}

// Caller must load state
provision_command.execute(created).await?;
let state = repository.load(&env_name)?.expect("Must exist");
let provisioned = match state {
    AnyEnvironmentState::Provisioned(env) => env,
    _ => return Err("Wrong state!"), // Runtime error!
};
```

**Why Rejected**:

- ❌ Loses compile-time type safety (runtime pattern matching required)
- ❌ Awkward data flow (caller must reload what command just created)
- ❌ Repository becomes central to data flow (not just persistence)
- ❌ Makes command chaining difficult
- ❌ Doesn't leverage the type-state pattern we built in Phases 1-4

### Alternative 2: Hybrid - Store Environment in Command

```rust
pub struct ProvisionCommand {
    environment: RefCell<Option<Environment<Provisioning>>>,
    // ...
}

impl ProvisionCommand {
    pub async fn execute(&self, environment: Environment<Created>) -> Result<(), Error> {
        let provisioning = environment.start_provisioning();
        *self.environment.borrow_mut() = Some(provisioning);

        // Execute...

        let provisioned = self.environment.borrow().as_ref().unwrap().complete_provisioning(ip);
        self.repository.save(&provisioned.into_any())?;
    }

    pub fn get_result(&self) -> Environment<Provisioned> {
        // Complex extraction logic...
    }
}
```

**Why Rejected**:

- ❌ Interior mutability complexity (`RefCell`, borrowing rules)
- ❌ Unclear ownership semantics
- ❌ Still requires separate getter method
- ❌ Makes command non-`Send` (problematic for async)
- ❌ More complex than straightforward transformation

### Alternative 3: Builder Pattern with Fluent API

```rust
provision_command
    .with_environment(created)
    .execute()
    .await?
    .get_provisioned_environment();
```

**Why Rejected**:

- ❌ More complex API than direct transformation
- ❌ Still needs to return state somehow
- ❌ Doesn't solve the fundamental return question

## Related Decisions

- [Type Erasure for Environment States]./type-erasure-for-environment-states.md - How we handle serialization while maintaining type safety
- [Actionable Error Messages]./actionable-error-messages.md - Error handling approach for commands
- Phase 1-4 implementation of type-state pattern in `Environment<S>`

## References

- **Type-State Pattern in Rust**: <https://cliffle.com/blog/rust-typestate/>

  - Demonstrates how to use Rust's type system for state machines
  - Our pattern follows this approach for environment lifecycle

- **CQRS Flexibility**: <https://martinfowler.com/bliki/CQRS.html>

  - While pure CQS says commands return void, CQRS patterns often return acknowledgments
  - Command can return identifiers or confirmation objects

- **Functional Programming Perspective**:

  - State transitions as pure transformations: `S -> T`
  - Commands as functions that transform state
  - Side effects (persistence) are secondary concerns

- **Rust Ownership Model**:

  - Returning transformed data is idiomatic in Rust
  - Ownership transfer makes data flow explicit
  - No implicit state mutations

- **Phase 1-4 Implementation**:
  - `docs/features/environment-state-management/feature-description.md`
  - `src/domain/environment/mod.rs` - Type-state implementation
  - `src/infrastructure/persistence/` - Repository layer

## Implementation Notes

### Persistence Error Handling

Persistence failures are logged but don't fail the command:

```rust
if let Err(e) = self.persist_state(&environment) {
    warn!(
        "Failed to persist state: {}. Command execution continues.",
        e
    );
}
```

**Rationale**: The in-memory state transformation is valid even if persistence fails. We log for observability but don't block the workflow.

### Command Chaining Pattern

This decision enables future orchestration:

```rust
// Phase 5 Subtasks 3-4: Individual commands return typed states
let provisioned = provision_cmd.execute(created).await?;
let configured = configure_cmd.execute(provisioned).await?;

// Future: Orchestration layer with compile-time guarantees
let workflow = Orchestrator::new()
    .step(provision_cmd)    // Requires Created, produces Provisioned
    .step(configure_cmd)    // Requires Provisioned, produces Configured
    .step(deploy_cmd);      // Requires Configured, produces Deployed

workflow.execute(created).await?; // Type-checked at compile time
```

### Backward Compatibility

Commands still work with existing E2E tests by extracting values from returned states:

```rust
// Old pattern (Phase 5 Subtask 1):
let ip_address = provision_command.execute(&ssh_credentials).await?;

// New pattern (Phase 5 Subtask 3+):
let provisioned = provision_command.execute(environment).await?;
let ip_address = provisioned.instance_ip(); // Getter method
```