
Orchestrating the Perfect FHIR Sandbox: The Power of Synthea and Flexporter
How to move beyond static test data by combining the scale of Synthea with the precision of Flexporter to build realistic, custom healthcare environments.
As a healthcare architect, one of the most frustrating scenarios is having to redeploy an entire FHIR server just to change a small validation rule or add a simple custom endpoint. In highly regulated environments, a deployment can trigger a massive QA cycle.
What if you could push logic changes as easily as you push data?
In this tutorial, we’ll look at how I implemented Dynamic Scripting using Groovy in my HAPI FHIR Groovy server.
The core idea is to leverage the FHIR Configuration resource as a container for code. A custom GroovyClassLoader monitors these resources and dynamically registers Spring beans (Interceptors and Providers) into the HAPI FHIR context.
Your script must be a valid Groovy class tagged with @CustomScript and standard HAPI annotations.
package scripting
import ca.uhn.fhir.interceptor.api.Hook
import ca.uhn.fhir.interceptor.api.Interceptor
import ca.uhn.fhir.interceptor.api.Pointcut
import com.gjergjsheldija.scripting.api.CustomScript
import groovy.util.logging.Slf4j
@Slf4j
@Interceptor
@CustomScript
class PatientLogger {
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
void execute(theResource) {
if (theResource instanceof org.hl7.fhir.r4.model.Patient) {
log.info("Interception! New patient being saved: ${theResource.name[0].family}")
}
}
}
Wrap your script in a FHIR Configuration resource. The name must match the class name.
POST /Configuration
Content-Type: application/fhir+json
{
"resourceType": "Configuration",
"name": "PatientLogger",
"type": "script",
"status": "active",
"body": "... (the script above) ..."
}
To tell the server to compile and register the new logic, call the $load-script operation:
POST /Configuration/$load-script
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{ "name": "name", "valueString": "PatientLogger" }
]
}
Logic that changes at runtime must be tested even more rigorously than static code. One of the best synergies in the Groovy world is the Spock Framework.
In our repository, you’ll find examples of how we test these dynamic scripts using Spock specifications. Because Groovy and Spock share the same DNA, you can write highly expressive tests that mock HAPI FHIR DAOs and verify clinical logic with ease.
Here is how you can test a complex calculation script by mocking the database layer:
class OutputCalculationTest extends Specification {
def "test calculatedOutput method"() {
given: "a mocked environment"
def observationDao = Mock(IFhirResourceDaoObservation)
def outputCalculation = new OutputCalculation(observationDao: observationDao)
and: "some sample FHIR data"
def observations = parser.parseResource(Bundle, observationsJson).entry.collect { it.resource }
observationDao.searchForResources(_, _) >> observations
when: "the calculation logic is triggered"
def result = outputCalculation.calculatedOutput(encounterRef, intervalMinutes, dateFrom, dateTo, requestDetails)
then: "the output matches clinical expectations"
result.entry.size() == 12
result.entry[0].resource.valueQuantity.value == 75.5
}
}
By keeping your tests in the src/test/java folder alongside your Java tests, you ensure that even your dynamic scripts are part of your core CI/CD quality gate.
Beyond being a first-class citizen in the Java ecosystem (JSR-223), Groovy offers a “nearly-native” performance once the scripts are compiled by the JVM’s JIT. For healthcare systems, this means we get the flexibility of scripting without the typical interpreted language overhead.
Injecting code at runtime is a double-edged sword. As architects, we must address the “elephant in the room”: Sandboxing.
java.lang.reflect or java.io directly).AuditEvent subsystem, ensuring a clear “chain of custody” for your server’s logic.From an architect’s perspective, this moves your FHIR server from being a “black box” to a programmable platform. It allows for:
Ready to dive deeper? Check the documentation in doc/scripting.md.