|
| 1 | +# ADR048: Immutable Form Versioning |
| 2 | + |
| 3 | +Date: 2026-03-24 |
| 4 | + |
| 5 | +## Status |
| 6 | + |
| 7 | +Accepted |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +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: |
| 12 | + |
| 13 | +- `GET /api/v2/forms/:form_id/draft` |
| 14 | +- `GET /api/v2/forms/:form_id/live` |
| 15 | +- `GET /api/v2/forms/:form_id/archived` |
| 16 | + |
| 17 | +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. |
| 18 | + |
| 19 | +This approach has several limitations: |
| 20 | + |
| 21 | +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. |
| 22 | +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. |
| 23 | +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. |
| 24 | + |
| 25 | +## Decision |
| 26 | + |
| 27 | +We will introduce an immutable versioning model for published forms, exposed through a new v3 API. The key changes are: |
| 28 | + |
| 29 | +### New API endpoints |
| 30 | + |
| 31 | + |
| 32 | +| Endpoint | Description | |
| 33 | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | |
| 34 | +| `GET /api/v3/forms/:form_id/draft` | Returns the current draft form document JSON (mutable, changes as the form creator edits) | |
| 35 | +| `GET /api/v3/forms/:form_id/versions/:form_version` | Returns an immutable, versioned form document. Once created, this content never changes. | |
| 36 | +| `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) | |
| 37 | + |
| 38 | + |
| 39 | +### Lifecycle |
| 40 | + |
| 41 | +- Draft state: A form being edited has a draft available at `/api/v3/forms/:form_id/draft`. This behaves similarly to today. |
| 42 | +- 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. |
| 43 | +- 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. |
| 44 | + |
| 45 | +### Content removal |
| 46 | + |
| 47 | +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). |
| 48 | + |
| 49 | +## Consequences |
| 50 | + |
| 51 | +### Positive |
| 52 | + |
| 53 | +- 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. |
| 54 | +- 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. |
| 55 | +- 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. |
| 56 | +- 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. |
| 57 | +- 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. |
| 58 | +- Audit trail. The full history of published form versions is preserved and addressable. |
| 59 | + |
| 60 | +### Negative |
| 61 | + |
| 62 | +- 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`). |
| 63 | + |
0 commit comments