Build1 distinct publisher3 min readUpdated
A Symfony bundle wires Doctrine's optimistic locking to HTTP If-Match, on the argument that a conditional-header check alone still leaves a race window before flush().
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
The interesting part of the design is the refusal to collapse the two checks into one. A conditional request handler that validates If-Match and then hands off to the controller has done something useful, but the author's own sequence spells out the hole: the header is checked, the version matches, another request writes the row, and only then does the controller call flush() [s1c8]. Everything between step one and step four is unguarded. So the bundle keeps the Doctrine version check as the thing that actually protects the write, and translates a Doctrine optimistic lock exception at flush time into 412 Precondition Failed [s1c9] so the client sees the same failure regardless of which of the two gates caught it.
That is the sane split. The database enforces atomicity on the row; HTTP enforces that the representation the client edited is still current when the request lands [s1c10]. Neither substitutes for the other, and a client that only ever hears about the second one gets an error code it can retry against rather than a silent overwrite [s1c1].
The status code taxonomy is where operators should look, because it is the part that reaches clients. A stale If-Match gets 412 [s1c6]. No If-Match at all gets 428 Precondition Required [s1c7], which is a policy decision, not a protocol requirement: the endpoint has decided that unconditional writes are not allowed rather than letting them through. Malformed conditional headers get 400 [s1c11]. Three distinct failure modes, three distinct codes, and any client library that treats them as one "it didn't work" bucket will retry the wrong one.
The honest caveat in the post is the one most implementations of this pattern skip. An ETag validates a representation, not a database row [s1c12]. Entity identity plus version is adequate only when nothing else moves the response, and the author lists what does move it: locale, serializer groups, related entities, user-specific fields, query parameters [s1c13]. If any of those change without bumping the entity version, the ETag is lying about what the client holds. The bundle's answer is an explicit scope string on both the read and write attributes, plus a pluggable EntityTagProviderInterface [s1c14], and the read example carries scope 'document-detail-v1' [s1c3] rather than defaulting to something implicit. Version the representation, not just the entity.
One small operational note: the default validator derives the tag from entity identity, Doctrine version and scope, but does not put the raw ID and version into the header [s1c5]. That keeps a monotonically increasing integer off the wire, which matters less for correctness than for what an ETag would otherwise leak about write volume on a resource.
The whole thing is roughly two attributes of surface area, #[EntityTag] on the read and #[RequireIfMatch] on the write [s1c3][s1c4], sitting on top of a Doctrine entity that already has #[ORM\Version] [s1c2]. That is cheap enough that the argument against it is not effort. It is that adding 428 to a write endpoint breaks every existing client that does not send If-Match, which is a versioning problem, not a concurrency one.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
Without a concurrency check, two clients reading the same resource can each write, and the second request overwrites the first without knowing the resource changed: the classic lost update problem.
Doctrine supports optimistic locking with a version field, declared with #[ORM\Version] on an integer column; the check makes the database update safe against concurrent changes between loading the entity and flushing it.
In the author's OptimisticConcurrencyBundle, a read endpoint is marked with #[EntityTag('document', scope: 'document-detail-v1')] and the response receives a strong ETag.
The write endpoint uses #[RequireIfMatch('document', scope: 'document-detail-v1')], and the client sends back the ETag it received on read as an If-Match header on the PATCH.
The bundle's default validator is based on entity identity, the Doctrine version and the optional representation scope; the actual database ID and version are not exposed directly in the header.
If the resource still has the same version the request continues normally; if somebody changed it in the meantime the response is 412 Precondition Failed.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Self-documented mechanism, no external verification
The mechanisms rest on well-established primitives (Doctrine's #[ORM\Version] optimistic locking; HTTP ETag/If-Match with 412/428/400 semantics) and the post supplies runnable-looking controller and entity code plus explicit response samples, which is real technical substance. But the cluster contains exactly one source, authored by the bundle's own creator, with no independent review, no tests or benchmarks demonstrating the closed race window, and no second publisher confirming the bundle behaves as described.
No usage evidence beyond the author's announcement
The only adoption-adjacent event is the author introducing his own bundle in the post. There are no releases with versions, no package-registry downloads, no stars, no named third-party deployments and no production usage disclosures, so adoption cannot be scored without inventing facts.
Claims stay slightly under the evidence
The framing is unusually restrained for a project announcement: the author refuses to claim the HTTP check alone is sufficient, keeps Doctrine's version check as the authority for the write, states that a version-derived ETag cannot describe representation-dependent responses, and declines to support hard DELETEs because Doctrine's DELETE omits the version from its WHERE clause. Claims are scoped to what the code plausibly does, so the modest negative reflects self-imposed limits rather than overstatement — offset only by the absence of any external validation.
Author promoting his own open-source bundle
The post is written by the creator of OptimisticConcurrencyBundle and functions as its introduction, so there is a clear promotional interest in the pattern and the package. That interest is disclosed in the first person ('I built OptimisticConcurrencyBundle'), the underlying primitives are standard rather than proprietary, and the supplied material shows no commercial product, pricing or sponsorship — which keeps the incentive moderate rather than severe.
Mechanism credible, breadth unproven
Confidence is moderate: the standard-primitive claims (Doctrine versioning, conditional-request status codes) are reliable and the reasoning about the pre-flush race window is coherent and internally consistent. It is held down by single-source, single-author sourcing, the complete absence of adoption or independent verification, and the fact that bundle-specific behavior is only as good as the author's description.
build
Thirty lines of Doctrine filter, and the query paths where it is simply not there1 distinct publisher
build
Your ETag polling budget assumes a token: unauthenticated 304s still cost GitHub quota1 distinct publisher
build
Postgres row-level security does nothing for the role your Symfony app connects with1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 22, 2026