Dual Runtime Architecture
One of Moco's most powerful features is its dual runtime support. The same workflow definition can run on two completely different execution engines without any code changes.
Why Dual Runtime?
Different stages of the development lifecycle have different requirements:
- Development & Testing: Fast iteration, easy debugging, minimal setup
- Production: Durability, scalability, fault tolerance, observability
Moco's dual runtime allows you to:
- Develop and test quickly with the in-memory runtime
- Deploy confidently to production with Temporal.io
- Use the same workflow definitions in both environments
In-Memory Runtime
The in-memory runtime is optimized for development and testing.
Characteristics
- Synchronous execution: Workflows run in the calling process
- No external dependencies: No need for Temporal server, databases, etc.
- Fast: Minimal overhead, instant startup
- Deterministic: Perfect for unit testing
- Limited durability: State is lost if process crashes
When to Use
- Local development
- Unit and integration tests
- CI/CD pipeline tests
- Quick prototyping
- Simple automation scripts
Example Usage
from moco.core.workflow.runtime.runtime_builder import RuntimeBuilder
from moco.core.workflow.client.client import WorkflowClient
# Build in-memory runtime
runtime_builder = RuntimeBuilder()
runtime = runtime_builder.build_in_memory_runtime()
# Create client
client = WorkflowClient(runtime)
# Execute workflow
result = await client.execute_workflow_from_yaml(
wfspec_yaml=workflow_yaml,
input_data={'param': 'value'}
)
Configuration
No configuration needed! Just use build_in_memory_runtime().
Temporal.io Runtime
The Temporal.io runtime is designed for production workloads.
Characteristics
- Asynchronous execution: Workflows run on distributed workers
- Durable: Workflow state persisted to database
- Fault tolerant: Automatic retries, workflow replay
- Scalable: Horizontal scaling of workers
- Observable: Complete execution history and monitoring
- Activity isolation: Activities run separately from workflow logic
When to Use
- Production deployments
- Long-running workflows (days, weeks, months)
- Mission-critical processes
- Workflows requiring high availability
- Complex distributed systems
Architecture
┌──────────────┐
│ Client │
└──────┬───────┘
│ Start Workflow
↓
┌──────────────────┐ ┌──────────────────┐
│ Temporal Server │────→│ PostgreSQL │
│ (Orchestrator) │ │ (State Storage) │
└────────┬─────────┘ └──────────────────┘
│
│ Task Queue
↓
┌──────────────────┐
│ Workflow Worker │
│ • Execute WF │
│ • Dispatch Acts │
└────────┬─────────┘
│
↓
┌──────────────────┐
│ Activity Worker │
│ • Execute Acts │
│ • Return Result │
└──────────────────┘
Example Usage
from moco.core.workflow.runtime.runtime_builder import RuntimeBuilder
from moco.core.workflow.client.client import WorkflowClient
# Build Temporal runtime (uses environment variables)
runtime_builder = RuntimeBuilder()
runtime = runtime_builder.build_temporal_runtime()
# Create client
client = WorkflowClient(runtime)
# Start workflow (non-blocking)
workflow_handle = await client.start_workflow(
workflow_name='my-workflow',
workflow_version='1.0.0',
workflow_id='unique-id-123',
input_data={'param': 'value'}
)
# Wait for result (or query later)
result = await workflow_handle.result()
Configuration
Set environment variables:
export MOCO_RUNTIME_TYPE=temporal
export MOCO_TEMPORALIO_ENDPOINT=localhost:7234
export MOCO_TEMPORALIO_NAMESPACE=moco
export MOCO_TEMPORALIO_TASK_QUEUE=default
Or use .env file:
MOCO_RUNTIME_TYPE=temporal
MOCO_TEMPORALIO_ENDPOINT=temporal.example.com:7233
MOCO_TEMPORALIO_NAMESPACE=moco
MOCO_TEMPORALIO_TASK_QUEUE=default
Starting Infrastructure
Use Docker Compose to run Temporal locally:
docker compose -f docker-compose-env.yml up -d
This starts:
- Temporal server (UI at http://localhost:8234)
- PostgreSQL (for Temporal state)
- Kafka (for event bus)
- RabbitMQ (for message queue)
Running Workers
Temporal requires workers to execute workflows:
# Start workflow/activity worker
.venv/bin/python -m moco_worker.main
Workers:
- Connect to Temporal server
- Poll task queues for work
- Execute workflow code
- Execute activities
- Report results back to Temporal
Runtime Selection
Moco automatically selects the runtime based on environment:
# Automatic runtime selection
runtime_builder = RuntimeBuilder()
runtime = runtime_builder.build_runtime() # Uses MOCO_RUNTIME_TYPE env var
You can also explicitly select:
# Explicit in-memory
runtime = runtime_builder.build_in_memory_runtime()
# Explicit Temporal
runtime = runtime_builder.build_temporal_runtime()
Key Differences
| Feature | In-Memory | Temporal.io |
|---|---|---|
| Execution Mode | Synchronous | Asynchronous |
| State Persistence | None | Database |
| Fault Tolerance | None | Automatic retry |
| Scalability | Single process | Distributed workers |
| Workflow History | No | Complete history |
| Activity Isolation | Same process | Separate workers |
| Setup Complexity | None | Requires infrastructure |
| Performance | Very fast | Higher latency |
| Use Case | Dev/Test | Production |
Activity Execution
Activities behave differently in each runtime:
In-Memory Runtime
- Activities execute in the same process
- No retry by default (unless specified)
- Failures propagate immediately
- Fast execution
Temporal Runtime
- Activities execute on activity workers
- Automatic retry with exponential backoff
- Failures are tracked in workflow history
- Configurable timeouts:
schedule_to_close_timeout: Max time from schedule to completionstart_to_close_timeout: Max time from start to completionschedule_to_start_timeout: Max time waiting in queue
Local Activity Execution
You can force activities to run locally (in workflow process) even with Temporal:
- activity:
type: builtin.simple_calculation
input_data:
value: "{{ x }}"
execute_locally: true # Run in workflow process
output_name: result
Use for:
- Very fast operations (< 1ms)
- Operations that don't benefit from retries
- Reducing worker overhead
Testing with Both Runtimes
Write tests that work with both runtimes:
import pytest
from moco.core.workflow.runtime.runtime_builder import RuntimeBuilder
@pytest.mark.parametrize("runtime_type", ["in_memory", "temporal"])
async def test_workflow(runtime_type):
runtime_builder = RuntimeBuilder()
if runtime_type == "in_memory":
runtime = runtime_builder.build_in_memory_runtime()
else:
runtime = runtime_builder.build_temporal_runtime()
client = WorkflowClient(runtime)
result = await client.execute_workflow_from_yaml(
wfspec_yaml=workflow_yaml,
input_data=test_input
)
assert result == expected_output
Best Practices
Development
- Use in-memory runtime for fast iteration
- Write unit tests with in-memory runtime
- Test critical paths with Temporal runtime
- Keep workflow definitions runtime-agnostic
Production
- Always use Temporal runtime in production
- Configure appropriate timeouts for activities
- Use separate task queues for different workflow types
- Monitor workflow execution via Temporal UI
- Set up alerts for workflow failures
Workflow Design
- Keep workflows deterministic
- Don't use non-deterministic operations in workflow code
- Push side effects to activities
- Use activities for external I/O
- Keep workflow logic focused on orchestration
Migration Path
Moving from development to production:
-
Develop: Use in-memory runtime
# No environment variables needed.venv/bin/pytest -
Test: Test with Temporal locally
docker compose -f docker-compose-env.yml up -dexport MOCO_RUNTIME_TYPE=temporal.venv/bin/python -m moco_worker.main &.venv/bin/pytest -m integration -
Deploy: Point to production Temporal
export MOCO_RUNTIME_TYPE=temporalexport MOCO_TEMPORALIO_ENDPOINT=temporal.prod.example.com:7233export MOCO_TEMPORALIO_NAMESPACE=production
No code changes required - just environment configuration!