Design Decisions

Key design constraints, architectural rationale, and extensibility guide for the Lab Forms module

👤 Ritu Kataria📅 Updated: Mar 13, 2026🏷️ feature

Design Decisions & Architecture


Key Design Decisions & Constraints

Preset System

Presets are "global template" processes with lab_id=NULL and process_type=Preset. They serve as starting points that can be cloned into lab-specific configs. Creation is restricted to a hardcoded list of allowed account manager emails.

A special subprocess type (for_communication=True) is only valid on consent forms. Questions within it must use reserved question codes. When values are captured, the system automatically updates PatientConsent flags (email, SMS, WhatsApp, fax, mail) with TTL-based expiry.

Only Patient process type on consent forms supports TTL. Expiry is calculated from created_at using ttl value and ttl_mode (days/months/years). Expired patient-level processes are excluded when creating consent for subsequent bills.

Iteration Support

Processes with requires_pdf_iterations=True support multiple iterations of responses. Each QuestionValue carries an iteration field. PDF generation can produce per-iteration documents.

Offline/Mobile Sync

The system supports offline mobile data collection with:

  • BulkQuestionValueView for batch sync
  • BulkQuestionValueSyncFailureView for logging sync failures to Elasticsearch
  • GuestBulkQuestionValue for unauthenticated (patient portal) submissions

Legacy instance_id on Process

The Process.instance_id column is being deprecated in favor of the ProcessLinkedInstances table. A TODO comment in the codebase marks this for removal.

Branch Scoping

For multi-branch labs:

  • Lab form history is filtered by bill.branch_id
  • MIS export uses Elasticsearch to resolve patient → branch mapping
  • Bill-level form details check branch authorization

Patient consent emails are sent on:

  • created — new consent form
  • resend — manual resend
  • linked_process — new processes added
  • revoked — consent revoked

Emails include a QR code (uploaded to S3) linking to the patient portal consent form. Email sending requires both consent_management feature flag and patient_consent_email_communication lab setting to be enabled.

Additional Patient Info — Patient-Scoped (Not Bill-Scoped)

Unlike aoe and consent which store captured data in QuestionValue (keyed by bill_id), additional_patient_info stores data in its own AdditionalPatientInfo table keyed by patient_id. This is because patient demographic/clinical info (gender identity, sexual orientation, etc.) persists across bills and should be captured once per patient, not once per billing event.

Consequences:

  • No LabForm runtime entityadditional_patient_info does not create LabForm or LabFormLinkedProcesses records. Data flows directly from the UI to AdditionalPatientInfo via UserDetails.handle_additional_patient_info().
  • No iteration support — since data is patient-level, there is no concept of multiple iterations.
  • No form_status — there is no pending/completed/revoked lifecycle. Values are simply created or updated.
  • Triggered on patient save — data capture is integrated into the patient registration/update flow (via UserDetails.after_save()), not triggered by a separate form submission event.

Additional Patient Info — Single Subprocess Constraint

Processes with form_type="additional_patient_info" are limited to exactly one subprocess. This is enforced in Process.after_save() — the process_type is also auto-set to "Patient" during validation, regardless of what the caller provides.

Additional Patient Info — Page Settings & Defaults

AdditionalPatientInfoSettings provides a mapping layer between form processes and UI pages. Each page type (appointment, registration, home_collection, cc_registration) can have multiple processes assigned, but only one default per (lab, page, custom_page) combination. The default process cannot be disabled. Custom registration pages are supported via the custom_page FK, gated by the allow_registration_custom_pages session flag.

Additional Patient Info — Access Control

  • Feature flag: lab_forms_management in session — required for all patient-facing and settings APIs
  • User flag: user_additional_patient_info_history_flag on LabUser — controls access to view history (mapped to LAB_USER_REGISTRATION_ACCESS permission group)
  • Custom page access: allow_registration_custom_pages session flag — required when custom_page_id is specified in settings

Architectural Rationale

One Engine, Infinite Form Types

The core motivation behind the Lab Forms architecture is building a single, generalised form engine rather than coding each form type as a separate feature. Consent, AOE and Additional Patient Info all look very different to end users, but structurally they are the same thing: a hierarchy of Processes → SubProcesses → Questions → Attributes with answers captured as QuestionValues.

By abstracting this structure once, every new form type the business needs in the future is just a new form_type string — no new tables, no new views, no new serializers. The entire config + capture + history + export + print pipeline works automatically.

Configuration Over Code

The system follows a configuration-driven philosophy:

  • Lab admins define forms entirely through the UI — no developer involvement needed to add a new question, change field order, toggle mandatory/hidden, or set up skip logic.
  • Question Attributes (validators, prefilling, skip conditions, options, date constraints) are stored as key-value metadata, not as columns. This means new attribute types can be introduced without schema migrations.
  • Presets provide global templates so CrelioHealth can ship best-practice form configurations out of the box while still letting each lab customise.

This dramatically reduces the cost of supporting 1000+ labs, each with different compliance and workflow requirements.

Entity-Agnostic Linking

The ProcessLinkedInstances + PROCESS_TYPE_MODEL_META_MAPPER pattern decouples form configuration from the specific entities it applies to. A single Process doesn't "know" whether it's attached to a Test, Profile, Sample, Promotion, Store, or Bill — the mapper resolves the correct Django model, lab-ID key, and validation rules at runtime.

This means:

  • New linkable entity types can be added by adding one entry to the mapper dict.
  • The same validation, caching, and serialization code path handles every entity type.
  • Business rules (e.g., "many processes per test in AOE") are expressed as simple conditionals, not duplicated codepaths.

Separation of Config-Time and Runtime

The architecture cleanly separates two concerns:

ConcernEntitiesWho uses it
Config-time ("what should the form look like?")Process, SubProcess, Question, QuestionAttribute, LinkedSubProcess, ProcessLinkedInstancesLab admins, account managers
Runtime ("what did the user actually fill in?")LabForm, LabFormLinkedProcesses, QuestionValue, PatientConsentPhlebotomists, patients, lab technicians

This separation means config changes don't corrupt existing captured data, and the same form definition can generate responses across thousands of bills without duplication of structure.

Robustness Highlights

  • Transactional integrity — Form config creation/updates are wrapped in @transaction.atomic so a failure mid-way through subprocess or question creation rolls back cleanly, preventing orphaned records.
  • Soft-delete everywhere — Processes, SubProcesses, and Questions use is_disabled rather than hard deletion, preserving referential integrity with historical QuestionValues.
  • Duplicate guards — Every level (Process, SubProcess, Question) enforces uniqueness checks scoped appropriately (per lab, per form type, per subprocess) to prevent configuration errors.
  • Skip logic & conditional flows — The attribute-based skip system supports complex branching (jump to another process, abort, restart with value retention) without hard-coding any workflow paths.
  • Offline-first design — Bulk sync endpoints, failure logging to Elasticsearch, and iteration support ensure mobile data collection works reliably even with intermittent connectivity.
  • Branch-aware multi-tenancy — The same lab form infrastructure supports both single-location and multi-branch labs with proper data isolation via branch scoping.

Reusability & Extensibility

What you can doHow
Add a completely new form typeAdd a string to lab_form_types tuple — all APIs, caching, MIS export, and history views work immediately
Add a new question field typeAdd to field_types tuple + optionally add a renderer mapping in FIELD_TYPE_MAPPER and a validation function in QuestionAttribute
Add a new linkable entityAdd one entry to PROCESS_TYPE_MODEL_META_MAPPER and optionally to instance_id_dependent_process_types
Add a new question attributeAdd to the attribute type lists and optionally implement a field-specific validator
Add new skip-to behavioursAdd to ALLOWED_SKIP_TO_TYPES list and handle in the renderer
Add a new prefilling sourceAdd to VALID_PREFILLING_SOURCES and implement the field resolver
Ship a default form for all labsCreate a Preset process (lab_id=NULL, process_type=Preset)

Why Not a Generic Form Builder Library?

While third-party form builders exist, this custom implementation was chosen because:

  1. Deep integration with domain entities — Forms need to link to tests, samples, profiles, bills, promotions, and home collections with lab-specific validation. No generic library handles this.
  2. LIMS-specific workflows — Skip logic that aborts a process and moves to the next sample, or restarts with retained values, is domain-specific branching that generic form engines don't support.
  3. Communication consent lifecycle — The tight coupling between consent form answers and PatientConsent flags with TTL-based expiry is a compliance requirement unique to healthcare.
  4. Offline-first mobile sync — The bulk value capture with iteration support, failure recovery, and per-subprocess sequencing was designed specifically for phlebotomists working in the field.
  5. Performance at scale — Redis-cached config, raw SQL for MIS export, and per-process hash caching are tuned for the platform's scale requirements — something a generic solution would require heavy customisation to achieve.
  6. Full audit trail — Every config change and value capture is activity-logged with category-level granularity, which is critical for lab accreditation and compliance.

On this page