Chapter 8: Going Async — The Server
Processing everything synchronously was fine for development. But in production, a slow compliance check should not block the deposit response. In this chapter we will configure Redis as the message broker, enable the outbox pattern for reliable delivery, and start the Protean server for asynchronous event processing.
The Outbox Pattern
When an aggregate raises events, they need to reach event handlers and projectors reliably. Protean uses the outbox pattern:
- When the Unit of Work commits, events are written to both the event store and an outbox table atomically.
- The outbox processor reads from the outbox table and publishes events to Redis Streams.
- StreamSubscriptions consume from Redis Streams and dispatch to handlers.
This guarantees at-least-once delivery — events are never lost, even if Redis is temporarily unavailable.
Configuration
Create a domain.toml file in your project directory:
[brokers.default]
provider = "redis"
url = "${REDIS_URL|redis://localhost:6379/0}"
event_processing = "async"
command_processing = "async"
enable_outbox = true
[event_store]
provider = "memory"
[server]
default_subscription_type = "stream"
messages_per_tick = 100
[server.stream_subscription]
blocking_timeout_ms = 100
max_retries = 3
retry_delay_seconds = 1
enable_dlq = true
Key settings:
brokers.default.provider = "redis"— use Redis as the message broker.event_processing = "async"— events flow through the broker instead of being processed inline.enable_outbox = true— reliable delivery via the outbox pattern.default_subscription_type = "stream"— useStreamSubscription(Redis Streams with consumer groups) for all handlers.enable_dlq = true— failed messages go to a dead-letter queue instead of being lost.
Starting Docker Services
You need Redis running:
docker run -d --name fidelis-redis -p 6379:6379 redis:7-alpine
Or use Protean's Docker Compose (if available):
make up
Starting the Server
The Protean server is a long-running process that polls Redis Streams and dispatches messages to handlers:
$ protean server --domain=fidelis
Starting Protean Engine...
Registered subscriptions:
AccountCommandHandler -> fidelis::account:command (StreamSubscription)
AccountSummaryProjector -> fidelis::account (StreamSubscription)
ComplianceAlertHandler -> fidelis::account (StreamSubscription)
NotificationHandler -> fidelis::account (StreamSubscription)
Engine running. Press Ctrl+C to stop.
Each handler gets its own consumer group in Redis. This means:
- Each handler maintains its own read position
- Multiple instances of the same handler can run in parallel (horizontal scaling)
- A slow handler does not block other handlers
- Failed messages are retried automatically before moving to the DLQ
How StreamSubscription Works
┌─────────────┐
│ Event Store │
└──────┬──────┘
│ (events written)
┌──────▼──────┐
│ Outbox │
└──────┬──────┘
│ (outbox processor publishes)
┌──────▼──────┐
│Redis Stream │
│(fidelis:: │
│ account) │
└──┬───┬───┬──┘
│ │ │ (consumer groups)
┌───────────┘ │ └───────────┐
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Projector │ │ Compliance │ │Notification │
│ Consumer │ │ Consumer │ │ Consumer │
│ Group │ │ Group │ │ Group │
└─────────────┘ └─────────────┘ └─────────────┘
Each consumer group reads independently. If the compliance handler is slow, the projector and notification handler continue at full speed.
StreamSubscription vs. EventStoreSubscription
| StreamSubscription | EventStoreSubscription | |
|---|---|---|
| Backed by | Redis Streams | Event store directly |
| Delivery | At-least-once via consumer groups | At-least-once via position tracking |
| DLQ | Built-in | Not yet available |
| Retries | Configurable with backoff | None |
| Use for | Production handlers | Development, projections |
For production systems, StreamSubscription is the recommended choice. It provides consumer groups, automatic retries, dead-letter queues, and horizontal scaling.
Sending Commands Asynchronously
With async processing enabled, domain.process() publishes the command
to the command stream instead of executing it immediately:
# This returns immediately — the command is queued
domain.process(
MakeDeposit(account_id=account_id, amount=500.00, reference="paycheck")
)
The server picks up the command from the Redis stream and dispatches it to the command handler asynchronously.
What We Built
- Redis as the message broker with
domain.tomlconfiguration. - The outbox pattern for reliable event delivery.
- StreamSubscription with consumer groups, retries, and DLQ.
- The Protean server (
protean server) for async processing. - An understanding of how events flow from the aggregate through the outbox to Redis Streams to handlers.
The system is now truly asynchronous. In the next chapter, we will add account-to-account transfers — a multi-aggregate workflow that requires a process manager.