medOS ultra

Specimen Transport Bounded Context

Separate from patient transport: temperature chain, time-to-lab, chain of custody, disjoint carrier pools.

3 min read diagramsUpdated 2026-05-26docs/architecture/specimen-transport-bounded-context.md

Status: Implemented (2026-05-27) DDD rationale: Specimen transport is a separate bounded context from patient transport (migration 044). The invariants diverge — temperature chain, time-to-lab limits, chain of custody, biohazard class — and the carrier pools are disjoint (runner/pneumatic tube/AGV/robot vs stretcher/wheelchair/gurney/ambulance).

Why Not Extend transport_request?

The “just add a subject_type discriminator” approach produces:

  • Nullable-column sprawl — specimens have no escort_required; patients have no temperature_zone
  • Per-type validation branches everywhere
  • Carrier dispatch logic that filters out wrong-domain carriers on every query
  • HL7 OML/FHIR Task integration that ignores 80% of rows

Instead: sibling aggregates with shared patterns (status lifecycle, pickup/dropoff geometry, append-only event log, ack-on-handoff, queue placement).

Entity Contracts

specimen_transport_request (aggregate root)

Status lifecycle:
  pending → assigned → in_transit → delivered → received_at_lab
                                  ↗ rejected
                     ↗ failed
  (any) → cancelled
Column Type Specimen-specific?
specimen_ids jsonb Yes — batch transport
temperature_zone text (ambient/refrigerated/frozen/body_temp) Yes
biohazard_class text (standard/bsl2/bsl3/radioactive) Yes
time_limit_minutes integer Yes — SLA enforcement
lab_request_id text Yes — links to MongoDB LabRequest
carrier_id uuid FK → specimen_carrier Yes — disjoint carrier pool
assigned_runner_id uuid Yes — lab runner, not porter

specimen_carrier (carrier registry)

Types: runner, pneumatic_tube, agv, robot, courier, manual

Completely disjoint from stretcher registry (patient transport).

specimen_transport_event_log (append-only audit)

Event types include specimen-specific entries: temperature_alert, custody_handoff.

Event Contract

Legacy → Normalized mapping:

Legacy Normalized
SPECIMEN_TRANSPORT_REQUESTED manifest.specimen_transport.requested
SPECIMEN_TRANSPORT_ASSIGNED manifest.specimen_transport.assigned
SPECIMEN_TRANSPORT_IN_TRANSIT manifest.specimen_transport.in_transit
SPECIMEN_TRANSPORT_DELIVERED manifest.specimen_transport.delivered
SPECIMEN_TRANSPORT_RECEIVED_AT_LAB manifest.specimen_transport.received_at_lab
SPECIMEN_TRANSPORT_REJECTED manifest.specimen_transport.rejected
SPECIMEN_TRANSPORT_FAILED manifest.specimen_transport.failed
SPECIMEN_TRANSPORT_CANCELLED manifest.specimen_transport.cancelled

Orchestrator Handlers

handleSpecimenTransportRequested (separate file)

Side effects:

  1. Insert department_queues row with dept_type='specimen_transport'
  2. Create acknowledgement_requests with tighter STAT escalation (3 min vs porter’s 5 min)

handleSpecimenTransportPipeline (inline in orchestrator)

Side effects:

  1. Update clinical_context.specimen_transport_tracking[transportId] with stage + timestamps
  2. Delegate to handleSpecimenTransportCompleted on terminal events
  3. Patch queue metadata for in-progress status updates

handleSpecimenTransportCompleted (separate file)

Side effects:

  1. Mark department_queues rows COMPLETED
  2. Close acknowledgement_requests

Queue Integration

The migration includes trg_sync_specimen_transport_queue — a Postgres trigger that auto-syncs specimen_transport_request status changes to department_queues with dept_type='specimen_transport'. This mirrors the billing-queue auto-sync pattern.

Backend Service

Lives in diagnostic service (DDD: specimen vocabulary = diagnostic domain).

File Purpose
services/diagnostic/.../specimenTransport/specimenTransport.module.ts NestJS module
services/diagnostic/.../specimenTransport/specimenTransport.service.ts Business logic
services/diagnostic/.../specimenTransport/specimenTransport.repository.ts MongoDB access
services/diagnostic/.../specimenTransport/specimenTransport.controller.mixin.ts Moleculer REST actions

REST Endpoints

Method Path Action
GET /specimen-transports/by-encounter/:encounterId List by encounter
GET /specimen-transports/by-lab-request/:labRequestId List by lab request
GET /specimen-transports/pending Active requests
POST /specimen-transports Create request
PUT /specimen-transports/:id/assign Assign runner + carrier
PUT /specimen-transports/:id/advance Move to next status
PUT /specimen-transports/:id/reject Reject with reason
PUT /specimen-transports/:id/fail Mark failed
PUT /specimen-transports/:id/cancel Cancel with reason

Frontend Service

web/src/services/medbase/specimenTransport.medbase.ts — Supabase read model client with column whitelist pattern. Same API shape as transport.medbase.ts but with specimen-specific types.

Shared Kernel (conventions, not code)

Both patient transport and specimen transport follow these patterns documented in docs/module-loop/BACKEND-CONTRACTS.md:

  • Status lifecycle enum with ASCII progression
  • Append-only event log with actor tracking
  • department_queues placement for worklist integration
  • acknowledgement_requests for escalation
  • Optimistic-lock claim pattern (first claim wins)

File Inventory

File Layer
infrastructure/medbase/migrations/20260527c_specimen_transport.sql Schema
infrastructure/medbase/functions/_shared/event-contract.ts Events
infrastructure/medbase/functions/encounter-orchestrator/index.ts Orchestrator routing + inline handler
infrastructure/medbase/functions/inpatient-handlers/handleSpecimenTransportRequested.ts Queue + ack creation
infrastructure/medbase/functions/inpatient-handlers/handleSpecimenTransportCompleted.ts Queue + ack closure
packages/platform-api-schema/src/diagnostic/specimenTransport/ TypeGoose entity
services/diagnostic/.../modules/specimenTransport/ Backend service module
web/src/services/medbase/specimenTransport.medbase.ts Frontend service
Ask Anything