From 7ec85b0da525e7d351fbbde93eb87fa48720a5d0 Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Tue, 24 Mar 2026 15:52:21 +0000 Subject: [PATCH 1/9] Add ADR048: Immutable Form Versioning Outlines the transition from a mutable document-based system to an immutable versioning model for published forms. This includes new API endpoints for draft and versioned forms, addressing limitations in the current system and enhancing caching, submission tracking, and user experience during form completion. --- ADR/ADR048-immutable-form-versioning.md | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 ADR/ADR048-immutable-form-versioning.md diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md new file mode 100644 index 000000000..0cc87929a --- /dev/null +++ b/ADR/ADR048-immutable-form-versioning.md @@ -0,0 +1,63 @@ +# ADR048: Immutable Form Versioning + +Date: 2026-03-24 + +## Status + +Accepted + +## Context + +GOV.UK Forms currently uses a mutable document-based system for managing form lifecycle states. The `Form` model has an associated `FormDocument` model, with documents tagged as `draft`, `live`, or `archived`. The current API (v2) exposes these via: + +- `GET /api/v2/forms/:form_id/draft` +- `GET /api/v2/forms/:form_id/live` +- `GET /api/v2/forms/:form_id/archived` + +The `FormStateMachine` manages transitions between states (`draft`, `live`, `live_with_draft`, `archived`, `archived_with_draft`), and the `FormDocumentSyncService` synchronises the JSON content into `FormDocument` records when these transitions occur. + +This approach has several limitations: + +1. The `/live` endpoint is mutable. The content behind `/api/v2/forms/:form_id/live` changes each time a form is re-published, so consumers must always re-fetch. This prevents effective caching. +2. No explicit link between a submission and the form version it was submitted against. When a form is updated and re-published, there is no reliable way to identify which version of the form a given submission relates to. This makes it difficult to group submissions by form version or detect when a form changed between batch submission deliveries. +3. Mid-journey disruption. If a form creator publishes a new version or archives a form while a user is part-way through filling it in, the form can change or disappear mid-journey. There is no mechanism for in-progress users to continue with the version they started. + +## Decision + +We will introduce an immutable versioning model for published forms, exposed through a new v3 API. The key changes are: + +### New API endpoints + + +| Endpoint | Description | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `GET /api/v3/forms/:form_id/draft` | Returns the current draft form document JSON (mutable, changes as the form creator edits) | +| `GET /api/v3/forms/:form_id/versions/:form_version` | Returns an immutable, versioned form document. Once created, this content never changes. | +| `GET /api/v3/forms/:form_id/latest` | Returns the latest live version of the form (a redirect or alias to the most recently published version) | + + +### Lifecycle + +- Draft state: A form being edited has a draft available at `/api/v3/forms/:form_id/draft`. This behaves similarly to today. +- Publishing (making live): When a form is made live, a new immutable version is created and assigned an incrementing version identifier (e.g. `1`, `2`, `3`). It becomes available at `/api/v3/forms/:form_id/versions/:form_version`. The `/api/v3/forms/:form_id/latest` endpoint points to this new version. +- Archiving: When a form is archived, `/api/v3/forms/:form_id/latest` and `/api/v3/forms/:form_id/draft` return `404` (or `410 Gone`). However, all previously published versions remain available at `/api/v3/forms/:form_id/versions/:form_version` because they are immutable. + +### Content removal + +Immutability prevents deletion as part of normal operations. A separate process will be needed for exceptional cases where published content genuinely must be removed (e.g. GDPR erasure). + +## Consequences + +### Positive + +- Cacheable published forms. Versioned form documents at `/api/v3/forms/:form_id/versions/:form_version` can be cached indefinitely by consumers (e.g. forms-runner, CDNs), significantly reducing load on the API and improving latency for form rendering. +- Submissions linked to form versions. Each submission can explicitly reference the `form_version` it was submitted against. This enables grouping submissions by version and helps people processing submissions to know exactly which questions were asked. +- Graceful publishing. Users who have already started filling in a form can continue submitting against the version they began with, even if the form creator publishes a new version in the meantime. +- Graceful archiving. When a form is archived, new users can be prevented from starting the form while users who have already started can finish and submit against the version they are on. +- Reverting to previous versions. Preserving all published versions makes it easier to implement future features allowing form creators to revert to a previous version of a form. +- Audit trail. The full history of published form versions is preserved and addressable. + +### Negative + +- Migration and data model complexity. Existing consumers (primarily forms-runner) will need to be updated to use the v3 API, requiring a transition period running both APIs in parallel. The `FormDocument` model (or a new model) will need to support version numbering alongside or in place of the current tag-based system (`draft`, `live`, `archived`). + From 81178545d589286dc91a9e16b3751ea13914b2ac Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Wed, 1 Apr 2026 17:32:04 +0100 Subject: [PATCH 2/9] Update ADR048 about hard submission deadlines Added a section on hard submission deadlines to ADR048, outlining the need for strict cutoff times for certain forms. This change clarifies that archiving will no longer serve as a method to prevent future submissions and suggests a new implementation approach for managing submission deadlines. --- ADR/ADR048-immutable-form-versioning.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md index 0cc87929a..b05cf9e25 100644 --- a/ADR/ADR048-immutable-form-versioning.md +++ b/ADR/ADR048-immutable-form-versioning.md @@ -42,6 +42,12 @@ We will introduce an immutable versioning model for published forms, exposed thr - Publishing (making live): When a form is made live, a new immutable version is created and assigned an incrementing version identifier (e.g. `1`, `2`, `3`). It becomes available at `/api/v3/forms/:form_id/versions/:form_version`. The `/api/v3/forms/:form_id/latest` endpoint points to this new version. - Archiving: When a form is archived, `/api/v3/forms/:form_id/latest` and `/api/v3/forms/:form_id/draft` return `404` (or `410 Gone`). However, all previously published versions remain available at `/api/v3/forms/:form_id/versions/:form_version` because they are immutable. +### Hard submission deadlines + +For legal or policy reasons, some forms may need a strict cutoff time after which no new submissions are permitted. Archiving would no longer act as a way to cut off in-progress journeys and prevent any future submissions. + +However, this behaviour can be re-implemented. For example, this could be a deadline timestamp attribute on the form that `forms-runner` checks before displaying the form or accepting a submission. This is more explicit and reliable than using archiving as a proxy for a hard stop. It would also allow form owners to schedule a cutoff in advance. + ### Content removal Immutability prevents deletion as part of normal operations. A separate process will be needed for exceptional cases where published content genuinely must be removed (e.g. GDPR erasure). @@ -60,4 +66,5 @@ Immutability prevents deletion as part of normal operations. A separate process ### Negative - Migration and data model complexity. Existing consumers (primarily forms-runner) will need to be updated to use the v3 API, requiring a transition period running both APIs in parallel. The `FormDocument` model (or a new model) will need to support version numbering alongside or in place of the current tag-based system (`draft`, `live`, `archived`). +- Archiving no longer acts as a way to cut off in-progress journeys and any future submissions. Some forms may rely on this behaviour for legal or policy reasons. We would need to re-implement this behaviour in a new way. From 0e661ca378872f969b6a12eb61d57dad0883545d Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Wed, 1 Apr 2026 17:44:42 +0100 Subject: [PATCH 3/9] Update ADR048 about linking submissions to form versions Added a section detailing how each submission will now store the `form_version` it was made against, helping proccess handle changes to the form. --- ADR/ADR048-immutable-form-versioning.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md index b05cf9e25..c0fc87f4a 100644 --- a/ADR/ADR048-immutable-form-versioning.md +++ b/ADR/ADR048-immutable-form-versioning.md @@ -42,11 +42,15 @@ We will introduce an immutable versioning model for published forms, exposed thr - Publishing (making live): When a form is made live, a new immutable version is created and assigned an incrementing version identifier (e.g. `1`, `2`, `3`). It becomes available at `/api/v3/forms/:form_id/versions/:form_version`. The `/api/v3/forms/:form_id/latest` endpoint points to this new version. - Archiving: When a form is archived, `/api/v3/forms/:form_id/latest` and `/api/v3/forms/:form_id/draft` return `404` (or `410 Gone`). However, all previously published versions remain available at `/api/v3/forms/:form_id/versions/:form_version` because they are immutable. +### Linking submissions to form versions + +Each submission will store the `form_version` it was made against, and that version information will be exposed to downstream form processors. This makes the version of the form explicit at the point of processing, instead of requiring processors to infer changes from the submission payload shape. + ### Hard submission deadlines For legal or policy reasons, some forms may need a strict cutoff time after which no new submissions are permitted. Archiving would no longer act as a way to cut off in-progress journeys and prevent any future submissions. -However, this behaviour can be re-implemented. For example, this could be a deadline timestamp attribute on the form that `forms-runner` checks before displaying the form or accepting a submission. This is more explicit and reliable than using archiving as a proxy for a hard stop. It would also allow form owners to schedule a cutoff in advance. +This behaviour should be re-implemented. For example, this could be a deadline timestamp attribute on the form that `forms-runner` checks before displaying the form or accepting a submission. This is more explicit and reliable than using archiving as a proxy for a hard stop. It would also allow form owners to schedule a cutoff in advance. ### Content removal From 238963119791fb7d374e60816f7b1c7f2c2b2dc1 Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Thu, 2 Apr 2026 15:27:39 +0100 Subject: [PATCH 4/9] Update ADR048 about schema versioning Added a section detailing schema changes --- ADR/ADR048-immutable-form-versioning.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md index c0fc87f4a..6b6328872 100644 --- a/ADR/ADR048-immutable-form-versioning.md +++ b/ADR/ADR048-immutable-form-versioning.md @@ -46,6 +46,14 @@ We will introduce an immutable versioning model for published forms, exposed thr Each submission will store the `form_version` it was made against, and that version information will be exposed to downstream form processors. This makes the version of the form explicit at the point of processing, instead of requiring processors to infer changes from the submission payload shape. +### Schema changes + +Consumers should always be able to treat `/api/v3/forms/:form_id/versions/:form_version` as a stable representation of the questions, structure, and behaviour that were published at that point in time. + +To handle changes in how form documents are represented, the form document should include an explicit `schema_version`. This makes it clear to consumers how to interpret the document while allowing the published content itself to remain immutable. + +In practice, schema changes should usually be backwards-compatible so consumers can continue to handle older and newer documents. Breaking schema changes should be reserved for the introduction of a new API version. + ### Hard submission deadlines For legal or policy reasons, some forms may need a strict cutoff time after which no new submissions are permitted. Archiving would no longer act as a way to cut off in-progress journeys and prevent any future submissions. @@ -71,4 +79,5 @@ Immutability prevents deletion as part of normal operations. A separate process - Migration and data model complexity. Existing consumers (primarily forms-runner) will need to be updated to use the v3 API, requiring a transition period running both APIs in parallel. The `FormDocument` model (or a new model) will need to support version numbering alongside or in place of the current tag-based system (`draft`, `live`, `archived`). - Archiving no longer acts as a way to cut off in-progress journeys and any future submissions. Some forms may rely on this behaviour for legal or policy reasons. We would need to re-implement this behaviour in a new way. +- Schema compatibility discipline. We would need to keep schema changes backwards-compatible within the API version wherever possible, and treat breaking schema changes as part of a future API version. From 7b08feecdb4b65c35f646a0d4def76702b2d0fc6 Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Tue, 12 May 2026 16:23:06 +0100 Subject: [PATCH 5/9] Refine ADR048 on immutable form versioning Clarified language regarding mutable endpoints and the explicit linking of submissions to form versions. Updated sections on hard submission deadlines and the implications of archiving, ensuring consistency in terminology and enhancing clarity for future implementation considerations. --- ADR/ADR048-immutable-form-versioning.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md index 6b6328872..4d256205c 100644 --- a/ADR/ADR048-immutable-form-versioning.md +++ b/ADR/ADR048-immutable-form-versioning.md @@ -18,8 +18,8 @@ The `FormStateMachine` manages transitions between states (`draft`, `live`, `liv This approach has several limitations: -1. The `/live` endpoint is mutable. The content behind `/api/v2/forms/:form_id/live` changes each time a form is re-published, so consumers must always re-fetch. This prevents effective caching. -2. No explicit link between a submission and the form version it was submitted against. When a form is updated and re-published, there is no reliable way to identify which version of the form a given submission relates to. This makes it difficult to group submissions by form version or detect when a form changed between batch submission deliveries. +1. The `/live` endpoint is mutable. The content behind `/api/v2/forms/:form_id/live` changes each time a form is republished, so consumers must always re-fetch it. This prevents effective caching. +2. There is no explicit link between a submission and the form version it was submitted against. When a form is updated and republished, there is no reliable way to identify which version of the form a given submission relates to. This makes it difficult to group submissions by form version or detect when a form changed between batch submission deliveries. 3. Mid-journey disruption. If a form creator publishes a new version or archives a form while a user is part-way through filling it in, the form can change or disappear mid-journey. There is no mechanism for in-progress users to continue with the version they started. ## Decision @@ -28,14 +28,12 @@ We will introduce an immutable versioning model for published forms, exposed thr ### New API endpoints - | Endpoint | Description | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `GET /api/v3/forms/:form_id/draft` | Returns the current draft form document JSON (mutable, changes as the form creator edits) | | `GET /api/v3/forms/:form_id/versions/:form_version` | Returns an immutable, versioned form document. Once created, this content never changes. | | `GET /api/v3/forms/:form_id/latest` | Returns the latest live version of the form (a redirect or alias to the most recently published version) | - ### Lifecycle - Draft state: A form being edited has a draft available at `/api/v3/forms/:form_id/draft`. This behaves similarly to today. @@ -56,9 +54,9 @@ In practice, schema changes should usually be backwards-compatible so consumers ### Hard submission deadlines -For legal or policy reasons, some forms may need a strict cutoff time after which no new submissions are permitted. Archiving would no longer act as a way to cut off in-progress journeys and prevent any future submissions. +For legal or policy reasons, some forms may need a strict cut-off time after which no new submissions are permitted. Archiving would no longer act as a way to cut off in-progress journeys and prevent future submissions. -This behaviour should be re-implemented. For example, this could be a deadline timestamp attribute on the form that `forms-runner` checks before displaying the form or accepting a submission. This is more explicit and reliable than using archiving as a proxy for a hard stop. It would also allow form owners to schedule a cutoff in advance. +This behaviour should be re-implemented. For example, this could be a deadline timestamp attribute on the form that `forms-runner` checks before displaying the form or accepting a submission. This is more explicit and reliable than using archiving as a proxy for a hard stop. It would also allow form owners to schedule a cut-off in advance. ### Content removal @@ -69,7 +67,8 @@ Immutability prevents deletion as part of normal operations. A separate process ### Positive - Cacheable published forms. Versioned form documents at `/api/v3/forms/:form_id/versions/:form_version` can be cached indefinitely by consumers (e.g. forms-runner, CDNs), significantly reducing load on the API and improving latency for form rendering. -- Submissions linked to form versions. Each submission can explicitly reference the `form_version` it was submitted against. This enables grouping submissions by version and helps people processing submissions to know exactly which questions were asked. +- Submissions grouped by form version. Each submission can explicitly reference the `form_version` it was submitted against, making it easier to group submissions by version for batch submissions. +- Version-aware processing. Processors can see which version a submission was made against and act accordingly, making downstream systems more resilient to change. - Graceful publishing. Users who have already started filling in a form can continue submitting against the version they began with, even if the form creator publishes a new version in the meantime. - Graceful archiving. When a form is archived, new users can be prevented from starting the form while users who have already started can finish and submit against the version they are on. - Reverting to previous versions. Preserving all published versions makes it easier to implement future features allowing form creators to revert to a previous version of a form. @@ -78,6 +77,9 @@ Immutability prevents deletion as part of normal operations. A separate process ### Negative - Migration and data model complexity. Existing consumers (primarily forms-runner) will need to be updated to use the v3 API, requiring a transition period running both APIs in parallel. The `FormDocument` model (or a new model) will need to support version numbering alongside or in place of the current tag-based system (`draft`, `live`, `archived`). -- Archiving no longer acts as a way to cut off in-progress journeys and any future submissions. Some forms may rely on this behaviour for legal or policy reasons. We would need to re-implement this behaviour in a new way. +- Archiving no longer acts as a way to cut off in-progress journeys and any future submissions. Some forms may rely on this behaviour for legal or policy reasons. We would need to re-implement this behaviour in a new way.[^hard-submission-deadline] - Schema compatibility discipline. We would need to keep schema changes backwards-compatible within the API version wherever possible, and treat breaking schema changes as part of a future API version. +### Notes + +[^hard-submission-deadline]: A possible implementation to support hard submission deadlines is to add a timestamp to the form to set the cut-off point. `forms-runner` should check this timestamp before showing the form or processing submissions. This would also let form owners schedule archiving in advance, instead of needing to archive the form manually at the exact time. From a65cff224e3a09af6ce09428799d99632803874b Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Tue, 12 May 2026 16:31:33 +0100 Subject: [PATCH 6/9] Update ADR048 to rename 'latest' endpoint to 'live' If we ever wanted to allow rolling back to an older form version. The "live" form, may point to an older version not the "latest". --- ADR/ADR048-immutable-form-versioning.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md index 4d256205c..240da915b 100644 --- a/ADR/ADR048-immutable-form-versioning.md +++ b/ADR/ADR048-immutable-form-versioning.md @@ -32,13 +32,13 @@ We will introduce an immutable versioning model for published forms, exposed thr | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `GET /api/v3/forms/:form_id/draft` | Returns the current draft form document JSON (mutable, changes as the form creator edits) | | `GET /api/v3/forms/:form_id/versions/:form_version` | Returns an immutable, versioned form document. Once created, this content never changes. | -| `GET /api/v3/forms/:form_id/latest` | Returns the latest live version of the form (a redirect or alias to the most recently published version) | +| `GET /api/v3/forms/:form_id/live` | Returns the live version of the form (a redirect or alias to the most recently published version) | ### Lifecycle - Draft state: A form being edited has a draft available at `/api/v3/forms/:form_id/draft`. This behaves similarly to today. -- Publishing (making live): When a form is made live, a new immutable version is created and assigned an incrementing version identifier (e.g. `1`, `2`, `3`). It becomes available at `/api/v3/forms/:form_id/versions/:form_version`. The `/api/v3/forms/:form_id/latest` endpoint points to this new version. -- Archiving: When a form is archived, `/api/v3/forms/:form_id/latest` and `/api/v3/forms/:form_id/draft` return `404` (or `410 Gone`). However, all previously published versions remain available at `/api/v3/forms/:form_id/versions/:form_version` because they are immutable. +- Publishing (making live): When a form is made live, a new immutable version is created and assigned an incrementing version identifier (e.g. `1`, `2`, `3`). It becomes available at `/api/v3/forms/:form_id/versions/:form_version`. The `/api/v3/forms/:form_id/live` endpoint points to this new version. +- Archiving: When a form is archived, `/api/v3/forms/:form_id/live` and `/api/v3/forms/:form_id/draft` return `404` (or `410 Gone`). However, all previously published versions remain available at `/api/v3/forms/:form_id/versions/:form_version` because they are immutable. ### Linking submissions to form versions From 3f613bc6de2eca34bacaac2688c010c4c2102e48 Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Tue, 12 May 2026 17:52:07 +0100 Subject: [PATCH 7/9] Add archived endpoint to ADR048 Realise we need to be able to find the archived version of the form to continue supporting the preview-archived form state. --- ADR/ADR048-immutable-form-versioning.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md index 240da915b..de946576f 100644 --- a/ADR/ADR048-immutable-form-versioning.md +++ b/ADR/ADR048-immutable-form-versioning.md @@ -33,12 +33,13 @@ We will introduce an immutable versioning model for published forms, exposed thr | `GET /api/v3/forms/:form_id/draft` | Returns the current draft form document JSON (mutable, changes as the form creator edits) | | `GET /api/v3/forms/:form_id/versions/:form_version` | Returns an immutable, versioned form document. Once created, this content never changes. | | `GET /api/v3/forms/:form_id/live` | Returns the live version of the form (a redirect or alias to the most recently published version) | +| `GET /api/v3/forms/:form_id/archived` | Returns the version that was live when the form was archived (a redirect or alias to that version) | ### Lifecycle - Draft state: A form being edited has a draft available at `/api/v3/forms/:form_id/draft`. This behaves similarly to today. - Publishing (making live): When a form is made live, a new immutable version is created and assigned an incrementing version identifier (e.g. `1`, `2`, `3`). It becomes available at `/api/v3/forms/:form_id/versions/:form_version`. The `/api/v3/forms/:form_id/live` endpoint points to this new version. -- Archiving: When a form is archived, `/api/v3/forms/:form_id/live` and `/api/v3/forms/:form_id/draft` return `404` (or `410 Gone`). However, all previously published versions remain available at `/api/v3/forms/:form_id/versions/:form_version` because they are immutable. +- Archiving: When a form is archived, `/api/v3/forms/:form_id/live` return `404` (or `410 Gone`). The `/api/v3/forms/:form_id/archived` endpoint points to the version that was live at the point of archiving. All previously published versions remain available at `/api/v3/forms/:form_id/versions/:form_version` because they are immutable. ### Linking submissions to form versions From 3438464e95664c107f117bf10eb398e0244323f5 Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Wed, 13 May 2026 09:23:05 +0100 Subject: [PATCH 8/9] Update ADR048 to have draft endpoint under /versions Renamed the draft endpoint to `/api/v3/forms/:form_id/versions/draft` to make it more consistent that everything under /versions returns a form document. --- ADR/ADR048-immutable-form-versioning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md index de946576f..32e175d74 100644 --- a/ADR/ADR048-immutable-form-versioning.md +++ b/ADR/ADR048-immutable-form-versioning.md @@ -30,14 +30,14 @@ We will introduce an immutable versioning model for published forms, exposed thr | Endpoint | Description | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `GET /api/v3/forms/:form_id/draft` | Returns the current draft form document JSON (mutable, changes as the form creator edits) | +| `GET /api/v3/forms/:form_id/versions/draft` | Returns the current draft form document JSON, as a special mutable form version. | | `GET /api/v3/forms/:form_id/versions/:form_version` | Returns an immutable, versioned form document. Once created, this content never changes. | | `GET /api/v3/forms/:form_id/live` | Returns the live version of the form (a redirect or alias to the most recently published version) | | `GET /api/v3/forms/:form_id/archived` | Returns the version that was live when the form was archived (a redirect or alias to that version) | ### Lifecycle -- Draft state: A form being edited has a draft available at `/api/v3/forms/:form_id/draft`. This behaves similarly to today. +- Draft state: A form being edited has a special mutable draft version available at `/api/v3/forms/:form_id/versions/draft`. This behaves similarly as the current draft endpoint. - Publishing (making live): When a form is made live, a new immutable version is created and assigned an incrementing version identifier (e.g. `1`, `2`, `3`). It becomes available at `/api/v3/forms/:form_id/versions/:form_version`. The `/api/v3/forms/:form_id/live` endpoint points to this new version. - Archiving: When a form is archived, `/api/v3/forms/:form_id/live` return `404` (or `410 Gone`). The `/api/v3/forms/:form_id/archived` endpoint points to the version that was live at the point of archiving. All previously published versions remain available at `/api/v3/forms/:form_id/versions/:form_version` because they are immutable. From 7c44e493efb441b50d65902469418046981eb708 Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Wed, 20 May 2026 12:16:01 +0100 Subject: [PATCH 9/9] Update ADR048 with Save and Return and submission form document benefits Document how immutable form versions make Save and Return easier to implement and remove the need to store a form document on each submission. --- ADR/ADR048-immutable-form-versioning.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ADR/ADR048-immutable-form-versioning.md b/ADR/ADR048-immutable-form-versioning.md index 32e175d74..93697217a 100644 --- a/ADR/ADR048-immutable-form-versioning.md +++ b/ADR/ADR048-immutable-form-versioning.md @@ -20,7 +20,7 @@ This approach has several limitations: 1. The `/live` endpoint is mutable. The content behind `/api/v2/forms/:form_id/live` changes each time a form is republished, so consumers must always re-fetch it. This prevents effective caching. 2. There is no explicit link between a submission and the form version it was submitted against. When a form is updated and republished, there is no reliable way to identify which version of the form a given submission relates to. This makes it difficult to group submissions by form version or detect when a form changed between batch submission deliveries. -3. Mid-journey disruption. If a form creator publishes a new version or archives a form while a user is part-way through filling it in, the form can change or disappear mid-journey. There is no mechanism for in-progress users to continue with the version they started. +3. Mid-journey disruption. If a form creator publishes a new version or archives a form while a user is filling it in or when they return later (e.g. Save and Return) the live form can change or disappear. There is no mechanism to pin in-progress users to the version they started. ## Decision @@ -45,6 +45,8 @@ We will introduce an immutable versioning model for published forms, exposed thr Each submission will store the `form_version` it was made against, and that version information will be exposed to downstream form processors. This makes the version of the form explicit at the point of processing, instead of requiring processors to infer changes from the submission payload shape. +Because each published version is immutable and addressable, we no longer need to store a full copy of the form document on every submission (as forms-runner does today) simply to guarantee that the form definition has not changed. Processors can resolve the correct document from `/api/v3/forms/:form_id/versions/:form_version` when needed. + ### Schema changes Consumers should always be able to treat `/api/v3/forms/:form_id/versions/:form_version` as a stable representation of the questions, structure, and behaviour that were published at that point in time. @@ -72,6 +74,8 @@ Immutability prevents deletion as part of normal operations. A separate process - Version-aware processing. Processors can see which version a submission was made against and act accordingly, making downstream systems more resilient to change. - Graceful publishing. Users who have already started filling in a form can continue submitting against the version they began with, even if the form creator publishes a new version in the meantime. - Graceful archiving. When a form is archived, new users can be prevented from starting the form while users who have already started can finish and submit against the version they are on. +- Implementing Save and Return. Pinning in-progress journeys to a `form_version` makes it easier to implement features such as Save and Return, where users leave part-way through and return later. +- No stored form document per submission. Submissions need only reference `form_version`; the form document can be retrieved reliably from the API when the submission is processed. - Reverting to previous versions. Preserving all published versions makes it easier to implement future features allowing form creators to revert to a previous version of a form. - Audit trail. The full history of published form versions is preserved and addressable.