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

Choosing a Pattern

Decision table

I need to...                                    → Use
─────────────────────────────────────────────────────────────────
Handle many independent clients                 → Task-per-Connection
Manage complex state without locks              → Actor Model
Process data through multiple stages            → Pipeline
Do the same thing to many items in parallel     → Fan-out / Fan-in
Keep services running despite crashes           → Supervisor Tree
Broadcast events to multiple consumers          → Event Bus / Pub-Sub

Choosing by symptom

Problem                              Solution
─────────────────────────────────────────────────────────────────
"I have lock contention"             → Actor (eliminate shared state)
"My pipeline stage is a bottleneck"  → Fan-out (parallelize that stage)
"Tasks crash and the system dies"    → Supervisor (auto-restart)
"I need to notify many components"   → Event Bus (decouple with broadcast)
"Each client needs isolated state"   → Task-per-Connection + Actor
"I process a stream of data"        → Pipeline with bounded channels

Combining patterns

Real systems use multiple patterns together. Here’s a typical web application:

┌──────────────────────────────────────────────────────────────┐
│  Web Application                                             │
│                                                              │
│  TCP Listener (task-per-connection)                          │
│    └── spawn(handle_request) for each HTTP request           │
│                                                              │
│  Request Handler                                             │
│    ├── Reads from Database Actor (actor model)               │
│    ├── Writes to Cache Actor (actor model)                   │
│    └── Emits metrics to Event Bus (pub-sub)                  │
│                                                              │
│  Background Jobs                                             │
│    ├── Job Queue → Workers (fan-out/fan-in)                  │
│    └── Supervised by a restart manager (supervisor tree)     │
│                                                              │
│  Log Pipeline (pipeline)                                     │
│    └── access log → parse → filter → ship to logging service │
│                                                              │
│  Metrics Dashboard (event bus subscriber)                    │
│    └── Receives metrics, displays live graphs                │
└──────────────────────────────────────────────────────────────┘

Pattern comparison

Pattern              Concurrency    State         Communication  Failure
─────────────────────────────────────────────────────────────────────────
Task-per-Connection  per client     per task      shared/channels dies alone
Actor                per entity     per actor     messages        isolated
Pipeline             per stage      per stage     channels        stage stops
Fan-out/Fan-in       per item       none shared   JoinSet         retry item
Supervisor           per worker     per worker    restart signal  auto-restart
Event Bus            per subscriber none shared   broadcast       drops msgs

Anti-patterns

Don’t spawn without joining

#![allow(unused)]
fn main() {
// BAD: fire and forget — leaked task, no error handling
tokio::spawn(async { do_work().await });

// GOOD: track the handle
let handle = tokio::spawn(async { do_work().await });
handle.await??; // propagate errors
}

Don’t use actors for everything

If you have a simple counter, Arc<AtomicU64> is better than an actor. Actors add overhead (channel, task, serialization). Use them when state is complex or when you’d hold a Mutex across .await.

Don’t use unbounded channels in production

#![allow(unused)]
fn main() {
// BAD: unbounded — OOM if consumer is slow
let (tx, rx) = mpsc::unbounded_channel();

// GOOD: bounded — backpressure if consumer is slow
let (tx, rx) = mpsc::channel(100);
}

Don’t block the executor

#![allow(unused)]
fn main() {
// BAD: blocks the worker thread
tokio::spawn(async {
    std::thread::sleep(Duration::from_secs(5)); // BLOCKS!
});

// GOOD: use async sleep or spawn_blocking
tokio::spawn(async {
    tokio::time::sleep(Duration::from_secs(5)).await; // yields
});

// GOOD: for CPU-heavy or blocking I/O
tokio::task::spawn_blocking(|| {
    std::fs::read("big-file.dat") // OK to block here
});
}