Thread Safety

Write workflow scripts that remain safe under concurrent processing.

Processing hooks may be invoked concurrently for different messages. Treat fields on the active script generation as shared state and keep message-specific state local to each hook invocation.

Concurrency model

Script fields are generation-wide

Values stored in fields of Main.groovy may be observed by multiple concurrent message-processing calls. A MessageEnvelope and the FHIR resources for one message belong to that processing operation and should not be retained after the hook returns.

Thread-safety rules

  • Avoid mutable shared state whenever possible.
  • Prefer local variables for per-message data.
  • Do not keep MessageEnvelope or per-message resource references in script fields.
  • If shared mutable state is truly required, use appropriate thread-safe structures.
  • Use SDK cache abstractions instead of custom static maps.
  • Initialize generation-wide resources in onStartup().
  • Release generation-owned external resources in onShutdown() when appropriate.

Safe and unsafe state

Safe: local per-message state
@Override
Bundle postConvert(MessageEnvelope envelope, Bundle bundle) {
    String messageId = envelope.messageControlId
    return enrich(bundle, messageId)
}
Unsafe: shared mutable field
private List<String> processedMessageIds = []

@Override
Bundle postConvert(MessageEnvelope envelope, Bundle bundle) {
    processedMessageIds << envelope.messageControlId
    return bundle
}

Message-local state

It is safe to modify the parsed HL7 message for the current message inside preConvert(). Do not store the MessageEnvelope, parsed HAPI message, generated Bundle, or resource references in generation-wide fields for later reuse.

HTTP client reuse

ScriptHttpClient instances are thread-safe and should normally be created once during onStartup() and reused across hook invocations.

ScriptHttpClient notificationClient

@Override
void onStartup() {
    notificationClient = httpClientFactory.create("notifications", notificationHttpProps)
}

Async callbacks

Keep asynchronous callback logic lightweight and independent. Do not mutate unsynchronized shared collections or rely on message-specific state after the original hook has continued. Logging, metrics, or self-contained result handling are safer callback responsibilities.

Generation-wide resources

Clients, immutable configuration, and SDK-managed caches are appropriate generation-wide objects. Create or prepare them during onStartup(), reuse them while the generation is active, and clean up resources owned by the script during onShutdown() when required.