The FHIR delete operation performs a "logical" delete, meaning the data is not physically removed from the database.
For example, suppose a Patient resource with ID 123 is created (via an HTTP POST /Patient) and subsequently deleted (via an HTTP DELETE Patient/123). This will cause a second version of the Patient/123 resource to be created with version Patient/123/_history/2 that is marked as deleted. This patient will no longer appear in search results, and attempts to read the resource (using an HTTP GET Patient/123) will fail with an HTTP 410 Gone response.
The original content of the resource is not destroyed however. It can still be found using two FHIR operations:
GET https://{baseurl}/Patient/123/_history/1.GET https://{baseurl}/Patient/123/_history.
The HTTP 410 Gone response may include a Location header which contains the fully qualified resource ID as well as the version ID. For example:
410 Gone
Location https://{baseurl}/Patient/123/_history/12
In this example, you can see that the deleted version of the resource is version 12. This means that the last non-deleted version is version 11 and this version can be accessed using a version-specific read to the following URL:
https://{baseurl}/Patient/123/_history/11
A common problem — frequently in test systems but sometimes in production systems, as well — is cleaning up batches of interdependent data.
For example, suppose you have a FHIR server containing a patient resource with the ID Patient/A. Suppose this FHIR server also contains Encounter/1 and Encounter/2, as well as Observation/3 and MedicationAdministration/4, and all of these resources have a reference to the resource Patient/A. We will call these resources the child resources.
If you try to DELETE Patient/A (using a standard FHIR DELETE operation), this request will be denied, assuming that there is a resource link between the child resources and the patient (a resource link is a field that is indexed with an active SearchParameter).
This will result in an HTTP 409 Conflict with a response like the following:
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "processing",
"diagnostics": "Unable to delete Patient/123 because at least one resource has a reference to this resource. First reference found was resource Observation/456 in path Observation.subject"
}
]
}
If you want to force a delete of Patient/A, you have several options:
The FHIR Transaction operation can be used to delete multiple resources at the same time. This is useful if you have chains or collections of resources to delete at once, but also can be used to delete circular references.
To delete multiple resources in a transaction, POST a Bundle such as the following to the root of your FHIR endpoint.
For Example:
POST https://{baseurl}
Content-Type: application/fhir+json
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{ "request": { "method": "DELETE", "url": "Organization/1" } },
{ "request": { "method": "DELETE", "url": "Organization/2" } }
]
}
Note: If any entry fails, the entire transaction is rolled back.
By default, Opala will block the deletion of a resource if any other resources have indexed references to the resource being deleted.
For example, suppose the resource Patient/123 has been saved in the repository, and a second resource Observation/456 is then saved as well, where the Observation.subject reference is a reference to the Patient. In this situation, attempts to delete Patient/123 will be blocked unless resources with references to this resource are deleted first (or are deleted as a part of the same transaction in the case of a transactional delete (see above)).
Admin note (reference-only): Some environments allow disabling or scoping referential-integrity checks for specific paths. This is not exposed on Opala’s public APIs; contact Opala if you need this in a managed/private environment.
There are two methods typically used for disabling checking:
The value for this setting is a set of one or more FHIRPath expressions, each one on a new line. For example, this setting could be set to Observation.subject in order to allow the deletion described above to proceed.
Certain environments can delete a resource and its dependents in one operation. This feature can be long-running and resource-intensive and is not exposed publicly on Opala’s public APIs. Information in this section is for-reference only and pertains to general administrative activities of a FHIR server.
With cascading deletes enabled, a user can perform the delete on Patient/A (per the example above) and all of the child resources will be deleted as well.
In order to perform a cascading delete, three things must occur:
_cascade) or a special header to indicate that a cascading delete is desired.
The following example shows a delete using a URL parameter:
DELETE https://{baseurl}/Patient/123?_cascade=deleteThe following example shows a delete using an HTTP header:
DELETE https://{baseurl}/Patient/123 X-Cascade: deleteDepending on the number of child resources linked to a given resource, the cascading delete can potentially take several seconds or even minutes to complete, and can consume considerable memory and processing resources during that time, which may have unexpected impacts on Opala. To minimize the duration and impact of cascading deletes, the actual deletes are performed in batches of a pre-configured size.
If after completing 10 passes of batched deletes, there are still child resources remaining, the deletes will be rolled back and Opala will return an error similar to:
Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count.If this type of error occurs after attempting a Cascading Delete request, the batch size can be increased by increasing the configured value of the allowed or expected child resource count deletions.
Some FHIR servers provide administrative endpoints to permanently remove deleted resources and/or history. These features are dangerous if misused, are not available on Opala’s public APIs, and—where enabled—require strict governance and elevated privileges.
In some cases, you may want to truly delete data; this might be because it was entered in error and should not be seen, because it represents a privacy concern to leave it in place, or because your solution does not require long term retention of stale data.
The $expunge operation can physically delete old versions of resources, deleted resources, or even all data in a database.
A “logical” delete offers a lower risk solution for removing resources; consider expunge only for tightly controlled remediation use cases.
The following categories of the $expunge operation are provided for reference only and are intended for educational, not operational, use. These operations require elevated privileges and careful access controls.
Some FHIR servers allow $expunge to be invoked on a single resource instance to permanently remove deleted or prior versions.
Example:
POST https://{baseUrl}/Patient/123/$expunge
Typical parameters may include:
A type-level $expunge targets all resources of a specific types.
Example:
POST https://{baseUrl}/Patient/$expunge
This operation can affect multiple resources and can be used to remove deleted or stale resources of that type as part of administrative cleanup.
A system-level $expunge acts across the entire FHIR repository, removing deleted or historical data across all resource types.
Example:
POST https://{baseUrl}/$expunge
This is the most comprehensive and potentially destructive variant. It is typically reserved strictly for internal remediation or data-purging use cases under system governance controls.
Some FHIR servers combine deletion and expunge in one step, such as:
DELETE https://{baseUrl}/Observation?status=cancelled&_expunge=truee
or
POST https://{baseUrl}/$delete-expunge
These operations perform permanent removal in the background as batch jobs and bypass standard authorization or audit checks.
Expunge and delete-expunge operations typically run as batch processes on the server.
The server deletes resources in batches (for example, groups of 100–1000 resources per transaction).
Large jobs may take several minutes to complete and can consume significant memory and database I/O.
If too many interdependent child resources remain after a fixed number of passes, the batch may roll back with an error such as:
Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count.
They are executed under strict governance policies to prevent accidental data loss, maintain referential integrity, and protect audit trails.
Because of these risks, Opala limits expunge operations to internal, administrative, and managed environments only.