Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR: Handler Composition Pattern

Status: Accepted Date: 2025-12 Ticket: TAS-112

Context

Cross-language step handler ergonomics research revealed an architectural inconsistency:

  • Batchable handlers: Already use composition via mixins (target pattern)
  • API handlers: Use inheritance (subclass pattern)
  • Decision handlers: Use inheritance (subclass pattern)
Current State:
✅ Batchable: class Handler(StepHandler, Batchable)  # Composition
❌ API: class Handler < APIHandler                    # Inheritance
❌ Decision: class Handler extends DecisionHandler    # Inheritance

Guiding Principle (Zen of Python): “There should be one– and preferably only one –obvious way to do it.”

Decision

Migrate all handler patterns to composition (mixins/traits), using batchable as the reference implementation.

Target Architecture:

All patterns use composition:
  Ruby:      include Base, include API, include Decision, include Batchable
  Python:    class Handler(StepHandler, API, Decision, Batchable)
  TypeScript: interface composition + mixins
  Rust:      trait composition (impl Base + API + Decision + Batchable)

Benefits:

  • Single responsibility - each mixin handles one concern
  • Flexible composition - handlers can mix capabilities as needed
  • Easier testing - can test each capability independently
  • Matches batchable pattern (already proven successful)

Example Migration:

# Old pattern (deprecated)
class MyHandler < TaskerCore::StepHandler::API
  def call(context)
    api_success(data)
  end
end

# New pattern
class MyHandler < TaskerCore::StepHandler::Base
  include TaskerCore::StepHandler::Mixins::API

  def call(context)
    api_success(data)
  end
end

Consequences

Positive

  • Consistent architecture: One pattern for all handler capabilities
  • Composable capabilities: Mix API + Decision + Batchable as needed
  • Testable in isolation: Each mixin can be tested independently
  • Matches proven pattern: Batchable already validates approach
  • Cross-language alignment: Same mental model in all languages

Negative

  • Breaking change: All existing handlers need migration
  • Learning curve: Contributors must understand mixin pattern
  • Migration effort: All examples and documentation need updates

Neutral

  • Pre-alpha status means breaking changes are acceptable
  • Migration can be phased with deprecation warnings

Ruby Result Unification

Ruby uses separate Success/Error classes while Python/TypeScript use unified result with success flag. Recommend unifying Ruby to match.

Rust Handler Traits

Rust needs ergonomic traits for API, Decision, and Batchable capabilities to match other languages:

#![allow(unused)]
fn main() {
pub trait APICapable {
    fn api_success(&self, data: Value, status: u16) -> StepExecutionResult;
    fn api_failure(&self, message: &str, status: u16) -> StepExecutionResult;
}

pub trait DecisionCapable {
    fn decision_success(&self, step_names: Vec<String>) -> StepExecutionResult;
    fn skip_branches(&self, reason: &str) -> StepExecutionResult;
}
}

FFI Boundary Types

Data structures crossing FFI boundaries must have identical serialization. Create explicit type mirrors in all languages:

  • DecisionPointOutcome
  • BatchProcessingOutcome
  • CursorConfig

Alternatives Considered

Alternative 1: Keep Inheritance Pattern

Continue with subclass pattern for API and Decision.

Rejected: Inconsistent with batchable; makes multi-capability handlers awkward.

Alternative 2: Migrate Batchable to Inheritance

Make batchable use inheritance to match others.

Rejected: Batchable composition is the better pattern; others should follow it.

Alternative 3: Language-Specific Patterns

Let each language use its idiomatic pattern.

Rejected: Violates cross-language consistency principle; increases cognitive load.

References