ADR: Actor-Based Orchestration Architecture
Status: Accepted Date: 2025-10 Ticket: TAS-46
Context
The orchestration system used a command pattern with direct service delegation, but lacked formal boundaries between commands and lifecycle components. This created:
- Testing Complexity: Lifecycle components tightly coupled to command processor
- Unclear Boundaries: No formal interface between commands and lifecycle operations
- Limited Supervision: No standardized lifecycle hooks for resource management
- Inconsistent Patterns: Each component had different initialization patterns
- Coupling: Command processor had direct dependencies on multiple service instances
The command processor was 1,164 lines, mixing routing, hydration, validation, and delegation.
Decision
Adopt a lightweight actor pattern with message-based interfaces:
Core Abstractions:
OrchestrationActortrait with lifecycle hooks (started(),stopped())Messagetrait for type-safe messages with associatedResponsetypeHandler<M>trait for async message processingActorRegistryfor centralized actor management
Four Orchestration Actors:
- TaskRequestActor: Task initialization and request processing
- ResultProcessorActor: Step result processing
- StepEnqueuerActor: Step enqueueing coordination
- TaskFinalizerActor: Task finalization with atomic claiming
Implementation Approach:
- Greenfield migration (no dual support)
- Actors wrap existing services, not replace them
- Arc-wrapped actors for efficient cloning across threads
- No full actor framework (keeping it lightweight)
Consequences
Positive
- 92% reduction in command processor complexity (1,575 LOC → 123 LOC main file)
- Clear boundaries: Each actor handles specific message types
- Testability: Message-based interfaces enable isolated testing
- Consistent patterns: Established migration pattern for all actors
- Lifecycle management: Standardized
started()/stopped()hooks - Thread safety: Arc-wrapped actors with Send+Sync guarantees
Negative
- Additional abstraction: One more layer between commands and services
- Learning curve: New pattern to understand
- Message overhead: ~100-500ns per actor call (acceptable for our use case)
- Not a full framework: Lacks supervision trees, mailboxes, etc.
Neutral
- Services remain unchanged; actors are thin wrappers
- Performance impact minimal (<1μs per operation)
Alternatives Considered
Alternative 1: Full Actor Framework (Actix)
Would provide supervision, mailboxes, and advanced patterns.
Rejected: Too heavyweight for our needs. We need lifecycle hooks and message-based testing, not a full distributed actor system.
Alternative 2: Keep Direct Service Delegation
Continue with command processor calling services directly.
Rejected: Doesn’t address testing complexity, unclear boundaries, or lifecycle management needs.
Alternative 3: Trait-Based Service Abstraction
Define Service trait and implement on each lifecycle component.
Partially adopted: Combined with actor pattern. Services implement business logic; actors provide message-based coordination.
References
- See the actor pattern implementation in
tasker-orchestration/ - Actors Architecture - Actor pattern documentation
- Events and Commands - Integration context