Conditional HTTP Writes with Entity Tags

A client reads a resource, edits its local copy, and sends a replacement several seconds later. During that interval another client may have committed a different replacement. A plain PUT has no statement about the representation on which the edit was based, so the server can accept a request whose starting state is already obsolete.

HTTP conditional requests can carry that missing premise. A response entity tag identifies a selected representation, and If-Match makes a later request conditional on a current representation matching one of the supplied tags. For state-changing methods, that turns representation identity into an explicit concurrency boundary.

The mechanism is narrower than a database transaction and more precise than a timestamp convention. It does not merge edits, serialize arbitrary business operations, or make a multi-resource update atomic. It gives the server a protocol-level condition that can reject a write when the representation observed by the client is no longer current.

The request carries an assumption about prior state

Consider a resource returned with a strong entity tag:

HTTP/1.1 200 OK
ETag: "v17"
Content-Type: application/json

{"status":"open","limit":120}

The tag is an opaque validator from the client’s perspective. The server controls its construction. "v17" might correspond to a stored revision, but HTTP does not require the tag text to expose a revision counter.

After editing the representation, the client can attach the validator to a replacement request:

PUT /accounts/42
If-Match: "v17"
Content-Type: application/json

{"status":"open","limit":150}

The request now says more than “store this body.” It states that the method is to proceed only if the current selected representation satisfies the If-Match precondition.

Suppose another write has already moved the resource to a representation tagged "v18". The precondition based on "v17" is false. Under HTTP conditional request semantics, a server that receives such a state-changing request evaluates the precondition before performing the method and can respond with 412 Precondition Failed.

That rejection preserves information that an unconditional replacement would discard: the submitted body was derived from an earlier representation.

Strong comparison matters for writes

Entity tags have strong and weak forms. A weak tag is prefixed with W/:

ETag: W/"catalog-17"

Weak validators can be useful when two representations are equivalent for cache validation even if they are not byte-for-byte identical. That looser relation is not sufficient for If-Match. HTTP specifies strong comparison for this precondition.

This distinction fits the concurrency role. A conditional write needs a validator that changes whenever the server considers the representation materially different for strong comparison. If a server emits the same strong tag for two representations that are not strongly equivalent, the tag cannot reliably separate those states at the HTTP boundary.

The tag-generation scheme therefore belongs to the resource model. A monotonically increasing database revision can be encoded as an entity tag. A digest can also serve in some designs, provided its construction satisfies the server’s representation identity requirements and does not introduce unacceptable collision semantics. The protocol treats the value as opaque; correctness depends on the server assigning validators consistently.

A modification timestamp can support other HTTP preconditions, but timestamps often have coarser semantics than an explicit revision. Two accepted changes within one timestamp resolution interval can share a displayed time even though the application treats them as distinct states. Entity tags avoid requiring clients to infer state identity from wall-clock time.

The comparison must guard the mutation

The HTTP header alone does not prevent a lost update. The server must connect precondition evaluation to the actual state transition.

A fragile implementation reads the current revision, compares it in application code, then performs an unconditional update:

read revision
compare with If-Match
update row

If another transaction can commit between the comparison and the update, both requests can observe the same revision and both can pass the application-level check. The protocol condition was evaluated, but it did not guard the mutation.

A relational database can often express the condition in the write itself:

UPDATE account
SET limit_value = :limit,
    revision = revision + 1
WHERE id = :id
  AND revision = :expected_revision;

If the affected-row count is zero, the application can distinguish a failed concurrency condition from a successful update, subject to its handling of missing resources and authorization. The compare and mutation occur as one conditional database statement.

Equivalent protection can come from a transaction with suitable locking or from a storage API that offers conditional writes. The exact mechanism varies. The invariant is stable: no competing write may change the guarded state between the successful comparison and the mutation it authorizes.

This is the point at which HTTP semantics and storage semantics meet. If-Match expresses the client’s condition; the persistence layer must enforce a corresponding condition without a race window.

Representation identity is not always resource identity

An entity tag validates a selected representation, not an abstract object detached from representation selection. That distinction becomes significant when one resource has multiple representations.

Content negotiation can produce JSON and another media type from the same underlying record. Compression and transformation can also affect representation data. A server’s validator strategy has to remain consistent with the representations it serves and with strong comparison rules.

For an API that exposes one canonical JSON representation per resource version, mapping a stored revision to a strong entity tag is straightforward. More elaborate representation pipelines need more care. Reusing one strong tag across outputs that are not strongly equivalent can make the validator claim more identity than the representations support.

This also separates entity tags from business version numbers. A domain object may have a revision that changes for every persisted mutation, while a particular representation omits some fields. The server can still choose a validator tied to the domain revision, making the representation validator change on mutations that are not visible in the body. That is conservative for concurrency: clients may receive a precondition failure even when their visible fields did not conflict.

A more selective validator can reduce such conflicts, but then its semantics must still match the state that the write is intended to protect. There is no protocol rule that chooses that boundary for the application.

Conditional writes detect conflicts; they do not resolve them

A 412 response establishes that the supplied precondition did not hold. It does not determine which value should win.

A client can fetch the current representation and present a conflict to a person, compute a domain-specific merge, abandon the edit, or submit a new operation based on the new state. Each choice carries application semantics beyond HTTP’s conditional request model.

This is especially important for partial updates. Two requests might change disjoint fields and still carry the same original entity tag. If one commits first, the second can fail even though the field-level edits could theoretically coexist. Resource-level validators intentionally treat the selected representation as the concurrency unit.

Applications that need field-level commutativity can model operations rather than replacement documents, maintain finer-grained versions, or define merge rules around domain semantics. Those designs trade a simple resource-wide condition for a more detailed conflict model.

The same limit applies across resources. An If-Match condition on /accounts/42 says nothing about the concurrent state of /limits/7. If an invariant spans both resources, a database transaction, conditional multi-item storage primitive, or another coordination mechanism must protect that invariant. HTTP preconditions do not create atomicity across independent requests.

Creation and deletion expose adjacent conditions

The If-Match wildcard form, If-Match: *, means the method is conditional on a current representation existing for the target resource. It can protect an operation that must not act on an absent resource without requiring the client to name a particular tag.

A related header, If-None-Match, expresses the inverse class of condition. With *, it can make creation conditional on the absence of a current representation. That is useful for APIs in which a client chooses the target URI and duplicate creation at that URI must be rejected.

These conditions are protocol statements, not substitutes for storage constraints. If uniqueness is a database invariant, the database still needs an atomic uniqueness mechanism. The HTTP precondition communicates intent and maps a failed condition into protocol behavior; the storage layer supplies race-free enforcement.

Deletion has the same concurrency shape as replacement. A client that fetched "v17" can send a conditional DELETE using If-Match: "v17". If the resource has changed to "v18", rejecting the deletion prevents an action based on an obsolete observation from silently removing the newer state.

A validator makes stale intent visible

Optimistic concurrency is often described as a storage technique, but APIs also need a place to carry the version assumption across the network. Without that information, the server cannot distinguish a deliberate overwrite of current state from a replacement computed from an old representation.

A strong entity tag paired with If-Match gives that assumption a standard HTTP form. Its value comes from the boundary it defines: the client names the representation it observed, the server compares that validator against current state, and the storage mutation preserves the comparison atomically.

The result is not automatic conflict resolution. It is a refusal to erase concurrency evidence before the application has a chance to interpret it.