70 lines
3.4 KiB
Markdown
70 lines
3.4 KiB
Markdown
# Sepsis Engine Design Decisions
|
|
|
|
## Why Redis for SIRS state, not a PostgreSQL time-range query
|
|
|
|
At a medium hospital with 200 concurrent inpatients, each generating five observations
|
|
per patient per minute, the sepsis engine processes approximately 17 observation events
|
|
per second at steady state.
|
|
|
|
A PostgreSQL alternative would look like this on every event:
|
|
|
|
```sql
|
|
SELECT observation_code
|
|
FROM observations
|
|
WHERE encounter_id = :encounterId
|
|
AND observation_code IN ('TEMP_C', 'HEART_RATE', 'RESP_RATE', 'WBC_K_UL')
|
|
AND recorded_at >= NOW() - INTERVAL '30 minutes'
|
|
AND (
|
|
(observation_code = 'TEMP_C' AND (value > 38.3 OR value < 36.0)) OR
|
|
(observation_code = 'HEART_RATE' AND value > 90) OR
|
|
(observation_code = 'RESP_RATE' AND value > 20) OR
|
|
(observation_code = 'WBC_K_UL' AND (value > 12.0 OR value < 4.0))
|
|
);
|
|
```
|
|
|
|
This query hits the `observations` table on every event. Under load it competes for
|
|
I/O with the ingest path writing new rows — both want the same composite index. With
|
|
Redis: four `SET`/`DEL` operations and one `MGET`, all O(1), all in-memory. No disk
|
|
I/O, no lock contention with the write path.
|
|
|
|
The TTL enforces the 30-minute sliding window automatically. Without Redis (or an
|
|
equivalent in-memory store), a background job would be needed to clean up stale
|
|
criteria — another failure point, another deployment concern.
|
|
|
|
## Why not Apache Flink
|
|
|
|
Flink is a distributed stream processor designed for stateful computation at scale
|
|
(millions of events per second across a fleet). It brings real costs:
|
|
|
|
- A Flink cluster (JobManager + TaskManagers) is infrastructure that must be deployed,
|
|
monitored, and upgraded independently of the application.
|
|
- Flink state backends (RocksDB, heap) add operational complexity that is not
|
|
justified unless the stream volume saturates what a single consumer thread can handle.
|
|
- Flink's exactly-once semantics require Kafka transactions, which add latency and
|
|
require tuning separate from the rest of the application.
|
|
|
|
At a single hospital (200 inpatients, ~17 observations/second), a Kafka consumer +
|
|
Redis state store handles the volume with single-digit millisecond latency per event
|
|
and no additional infrastructure. The trade-off: if this system needed to scale to a
|
|
multi-hospital network with 50,000+ concurrent inpatients (~5,000 observations/second),
|
|
Flink would become the right choice. The architecture decision is correct at this scale
|
|
and defensible at interview with a clear scale inflection point named.
|
|
|
|
## Phase 27 — Migration from SIRS to SOFA
|
|
|
|
**Rationale:** Sepsis-3 (2016) replaced SIRS with SOFA for organ dysfunction assessment.
|
|
SIRS is non-specific (post-exercise tachycardia, mild fever). Doctor feedback aligned with
|
|
moving sepsis **confirmation** to SOFA delta ≥ 2 while retaining qSOFA as a **bedside screen**.
|
|
|
|
**What changed:**
|
|
- `SirsDetector` / `SirsEvaluator` deleted — no new `SEPSIS_WARNING` alerts
|
|
- qSOFA ≥ 2 → `QSOFA_SCREEN` (WARNING, suppressible) with lab-order recommendation
|
|
- Sepsis hour-1 bundle triggers from `SOFA_SEPSIS` only (Phase 26 delta ≥ 2)
|
|
- Historical `SEPSIS_WARNING` and `QSOFA_WARNING` rows remain queryable
|
|
|
|
**Redis key patterns after Phase 27:**
|
|
- `qsofa:{encounterId}:{code}` — qSOFA screening (30 min TTL)
|
|
- `sofa:{encounterId}:{code}` — SOFA lab carry-forward (Phase 26)
|
|
- `gcs:{encounterId}:{code}` — GCS components (Phase 25)
|
|
- ~~`sirs:{encounterId}:{code}`~~ — removed (legacy keys expire)
|