0044 - API standards for updating and deleting resources
| ID: | ADR-0044 |
|---|---|
| Status: | PROPOSED |
| Published: | 2026-10-05 |
Notation
This ADR uses RFC 2119 keywords (MUST, MUST NOT,
SHOULD, SHOULD NOT, MAY) deliberately.
Context and problem statement
ADR-0036 requires service-to-service APIs to conform to a documented set of standards, and ADR-0039 builds those standards on IETF RFCs. This ADR sets how resources are updated, how conflicting updates are detected, and how resources are deleted. Payload shape and errors are set by ADR-0040.
Considered options
- Updates:
PUTwith complete replacement, partial updates discouraged and done withPATCHwhere supported (chosen);PATCHonly, as in JSON:API; bothPUTandPATCHrequired on every resource; RFC 6902 JSON Patch as the partial-update format; field masks, as in Google AIP-134; no partial updates at all. - Optimistic concurrency: a
versionfield and409 Conflict(chosen); RFC 9110 conditional requests (ETag,If-Match,412); comparingupdatedAttimestamps; last write wins. - Deletes:
DELETE, with a convention for soft deletes where a service needs them (chosen); hard deletes only; soft deletes through a status field and actions.
Decision outcome
Chosen options are marked above.
Updating resources
- The method
MUSTbePUT, with complete-replacement semantics, and the pathSHOULDbe/api/v1/{resource plural}/{id}. - APIs that support create-or-update
MUSTdo so throughPUT. - Resources
SHOULDcarryupdatedByandupdatedAt. - On success, APIs
SHOULDreturn200, or201if the request created the resource, butMAYreturn202or204. A200or201carries the latest representation of the resource, a202the job scheduled to update it (see ADR-0045), and a204nothing.
PUT is preferred because it is simpler to implement, far more common, and suits the
read-modify-write pattern client teams already use. Services that offer PATCH usually end up
offering PUT too, which is two endpoints doing the same thing.
Partial updates
- Services
SHOULD NOTsupport partial updates. A service that doesMUSTusePATCH. - In a partial update, an absent field means "do not change", and an explicit
nullclears the value. - On success, APIs
SHOULDreturn200butMAYreturn202or204. - To add or remove one element of a collection, an action (see ADR-0045) is simpler and preferred,
as in
POST /api/v1/groups/{id}/actions/add-collection. APIsMAYinstead implement RFC 6902 JSON Patch.
These semantics are credited to RFC 7396 JSON Merge Patch, with one departure. RFC 7396 stores an empty string as an empty string. These standards treat an empty string as not present, so the field is left unchanged. The reasons:
- One rule everywhere. An empty string means "not present" in every request (see ADR-0040). Following RFC 7396 would make partial updates the only exception.
- Nothing lost. A caller can still set a value, clear it with
null, or leave it unchanged by omitting it. Only storing an empty string cannot be expressed, and these standards never store one. - Just as easy to adopt. A caller using a standard merge patch library is unaffected unless it sends an empty string.
Optimistic concurrency
- APIs that update resources
SHOULDdetect conflicting updates. Those that doMUSTuse aversionfield markedrequiredand notreadOnly. versionSHOULDbe an integer incremented on every successful update, andMUSTbe formatted as a string whatever its value.- If the value sent does not match the stored version, the API
MUSTreturn409 Conflict. - On every successful update, the API
MUSTchange the version.
This departs from RFC 9110's conditional requests, for these reasons:
- Described by the same RFC. RFC 9110 gives this exact case as its example of
409: a representation sent withPUTthat conflicts with an earlier change, where versioning is in use. Conditional requests are an alternative, not a requirement. - Simpler for callers. The version is part of the resource the caller already reads and sends back. There is no header to capture and replay, and generated clients expose it as an ordinary property.
- Works for bulk operations. A bulk replace carries many resources, each with its own version.
One
If-Matchheader cannot. - Travels with the data. The version stays with the resource in events, local copies, logs, and caches.
- Nothing lost. A service can still return
ETagfor HTTP caching.
Deleting resources
- The method
MUSTbeDELETE, and the pathSHOULDbe/api/v1/{resource plural}/{id}. - On success, APIs
SHOULDreturn204butMAYreturn202with the job scheduled to delete the resource.
Services MAY support soft deletes. Those that do SHOULD follow these rules:
DELETEsoft-deletes by default, and read-many APIs exclude soft-deleted resources by default.- Read-one APIs
SHOULDreturn404for a soft-deleted resource unless the caller passesincludeDeleted=true. - Callers request a hard delete with
permanent=true, and ask for soft-deleted resources to be included withincludeDeleted=true. - Soft-deleted resources
SHOULDcarrydeletedByanddeletedAt.
DELETE /api/v1/users/62bed180-1f78-45d4-8a56-c996936a2947?permanent=true
GET /api/v1/users?includeDeleted=true
Positive consequences
- Updates follow one simple model that generated clients and client teams already understand.
- Lost updates are detected with an ordinary field, which works the same way for single and bulk updates.
- Soft deletes, where a service has them, behave the same way everywhere.
Negative consequences
- A caller changing one field of a large resource sends the whole resource.
- Two departures from RFCs, the empty-string rule and the
versionfield, need explaining to anyone who expects RFC 7396 or conditional requests.