Files
vigilcare-clinical/docs/decisions/sepsis-engine-design.md
T
voltsrage bf46e6554a feature: Sepsis Engine Refactor: Remove SIRS, Rewire qSOFA + Bundle
Frontend: GCS Entry, SOFA Display, Sepsis UI Refactor
2026-06-21 03:56:27 +08:00

3.4 KiB

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:

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.

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)