This document explains when and why to use each pattern, illustrated with Mermaid diagrams.
Shreedhar Kodate·22 December 2025·5 min read
python-engg-tmplatesdocs
This document explains when and why to use each pattern, illustrated with
Mermaid diagrams.
1. Python Concurrency Models
Python offers three concurrency primitives. The right choice depends on
whether your bottleneck is I/O, CPU, or both.
flowchart TD
Start([Bottleneck type?])
Start -->|"I/O-bound\n(network, disk, DB)"| IO[asyncio / Threading]
Start -->|"CPU-bound\n(compute, numpy)"| CPU[Multiprocessing]
Start -->|"Mixed"| Mixed[Multiprocessing + asyncio workers]
IO --> AsyncIO["asyncio\n• Single thread\n• Cooperative multitasking\n• Best for 1000s of tasks\n• Zero OS overhead"]
IO --> Threads["threading\n• OS threads\n• GIL released during I/O\n• Simpler mental model\n• Best for 10-100 tasks"]
CPU --> MP["multiprocessing\n• Separate processes\n• True parallelism\n• Bypasses GIL\n• Higher memory cost"]
AsyncIO --> GIL["GIL (Global Interpreter Lock)\nOne Python bytecode at a time\nReleased during I/O syscalls\nNot released for pure Python compute"]
Threads --> GIL
MP --> NoGIL["No GIL constraint\n(each process has its own GIL)"]
When to use what:
Workload
Best tool
Why
HTTP requests, DB queries
asyncio
Cooperative, zero thread overhead
File I/O, subprocess
threading or asyncio
GIL released during I/O
Numerical computation
multiprocessing
True CPU parallelism
ML inference batch
ProcessPoolExecutor
Load model once per worker
Streaming large files
async for + generator
Constant memory
2. Observer / EventBus Pattern
The Observer pattern decouples event producers from consumers. The EventBus
variant allows many-to-many subscriptions without direct object references.
sequenceDiagram
participant Producer as UserService
participant Bus as EventBus
participant M as MetricsCollector
participant A as AuditLogger
Producer->>Bus: publish("user.login", {user_id: "u-001"})
Bus->>M: on_event("user.login", data)
M-->>Bus: ok
Bus->>A: on_event("user.login", data)
A-->>Bus: ok
Bus-->>Producer: notified=2
Key design choices:
Handlers are stored as weakref.WeakMethod so dead subscribers are
automatically pruned (no memory leaks).
Failed handlers log an error but do not prevent other handlers from running.
The bus is synchronous; for async dispatch, wrap publish in asyncio.create_task.
3. Strategy Pattern Class Hierarchy
Strategy separates what algorithm to use from how to invoke it, making
algorithms interchangeable at runtime.
classDiagram
class SamplingStrategy {
<<abstract>>
+sample(dataset, n) list
}
class RandomSampling {
-rng: Random
+sample(dataset, n) list
}
class StratifiedSampling {
-label_key: str
-rng: Random
+sample(dataset, n) list
}
class WeightedSampling {
-weight_key: str
+sample(dataset, n) list
}
class DataSampler {
-strategy: SamplingStrategy
+set_strategy(strategy)
+draw(dataset, n) list
}
SamplingStrategy <|-- RandomSampling
SamplingStrategy <|-- StratifiedSampling
SamplingStrategy <|-- WeightedSampling
DataSampler o-- SamplingStrategy : uses
When to use Strategy:
Multiple algorithms for the same task (sorting, sampling, pricing)
Why jitter? Without jitter, all N clients that fail simultaneously will
retry at the same time (thundering herd). Uniform jitter [0, wait] spreads
retries across time, reducing peak load by ~50%.
5. Production Service Architecture
flowchart TB
subgraph "startup"
A[Load config.yaml] --> B[Setup logging\nRotatingFileHandler + QueueHandler]
B --> C[Install signal handlers\nSIGTERM / SIGINT]
C --> D[Register metrics\nCounter + Gauge + Histogram]
D --> E[Service READY]
end
subgraph "main loop"
E --> F{Shutdown\nrequested?}
F -->|No| G[Fetch batch\nfrom source]
G --> H[Validate records\nValidationError on failure]
H --> I[Process batch\n_process_batch]
I --> J[Record metrics\nlatency, errors, count]
J --> K{Every N batches}
K -->|Yes| L[gc.collect\nrelease memory]
K -->|No| F
L --> F
end
subgraph "shutdown"
F -->|Yes| M[Finish current batch]
M --> N[Flush log queue]
N --> O[Log metrics report]
O --> P[gc.collect final]
P --> Q([Process exits 0])
end
subgraph "signals"
R[SIGTERM / SIGINT] -->|sets shutdown flag| F
end
Key properties of the production service:
Graceful shutdown: in-flight batch completes before exit.
Why QueueHandler? File I/O on the hot path (inside a request handler or
tight loop) adds latency. The QueueHandler puts the log record onto an
in-memory queue and returns immediately. The QueueListener drains the queue
in a dedicated daemon thread, so disk latency never affects application code.