Build1 distinct publisher2 min readPublished
A dev.to writeup finds per-statement retry unimplementable, and reports a serialization test that passed on Postgres 14, 16 and 17 but failed on 15.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
The isolation knob and the retry loop sit at different levels of the stack, and that is the whole problem. A decorator can take an isolation level as an argument [3], so SERIALIZABLE is cheap to switch on. The loop cannot go in the same place, because it has to wrap the call to the decorated function, not live inside it. TypeORM has the same boundary: `dataSource.transaction()` owns the callback, and the retry belongs to whatever called it [9]. Read that way, the open request for auto retry on deadlock and serialization errors [2] is asking a library to do something from a position where it cannot see the transaction it would need to restart.
The naive fix is not neutral. Postgres aborts the transaction rather than the statement when it raises 40001 [4], and every statement afterwards on that connection comes back as 25P02, commands ignored until end of transaction block [5]. So a three-attempt wrapper around one UPDATE fires two doomed retries, sleeping 50 milliseconds and then 100 between them, and finally throws 25P02 [6][7]. You have spent 150 milliseconds [7] to replace the one error code that tells an operator what to do with one that tells them nothing.
The version result is the part worth keeping even if nobody in your shop touches SERIALIZABLE. The author's test asserted that the failure surfaces at COMMIT, which is a reasonable inference from how SSI tracks read and write dependencies [10]. It passed on 14, 16 and 17, and failed on 15 [11]: three of four tested majors agreed with a model that was wrong [12]. Postgres raises 40001 as soon as its machinery spots the dangerous structure, which can be at commit after every statement has already returned success, or at the conflicting statement, depending on the version, the plan and the interleaving [13]. The replacement test asserts only that the failing query was the COMMIT or the conflicting UPDATE [14], which is the honest invariant. A suite pinned to a single minor version would have shipped the wrong one as fact.
What is left is unglamorous. The caller rolls back, takes a fresh connection and transaction, and re-runs the entire callback from the top [9]. The cost moves to whoever wrote that callback, because its contents now run more than once and have to survive it. The ecosystem's current answer, per the same writeup, is to stay on READ COMMITTED and not think about write skew [8], which is cheaper until an application needs the guarantee it declined to turn on.
Ranked by verification strength, evidence, and original report placement.
The author's conclusion is that retry must sit with whatever called dataSource.transaction(): roll back, open a fresh connection and transaction, and re-run the entire callback from the top, making whole-transaction retry the only sound design and per-statement retry unimplementable.
That test passed on PostgreSQL 14, 16 and 17, and failed on PostgreSQL 15.
The PostgreSQL manual instructs that on receiving this error, an application should abort the current transaction and retry the whole transaction from the beginning.
TypeORM issue #9806, "Auto Retry options on error in transactions (e.g. Deadlock)", has been open since February 2023 with thirty thumbs-up reactions, six comments and no implementation.
typeorm-transactional, at 188,000 downloads a week, ships @Transactional() with isolation levels and seven propagation modes and no retry at all.
When PostgreSQL raises 40001 it does not fail the individual statement; it aborts the entire transaction, leaving the connection in a failed transaction state.
Follow any of these and your For You feed starts watching them — no settings page required.
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.
Mechanically grounded, single-source and unreplicated
The core mechanism is well specified and internally verifiable: the article quotes the PostgreSQL manual's whole-transaction retry instruction, names the exact SQLSTATEs (40001, 25P02, 40P01), and shows the failing and corrected code paths, so the deduction that per-statement retry cannot work is easy to follow. But the cluster has exactly one publisher and one author, and the load-bearing empirical result — pass on 14/16/17, fail on 15 — is self-reported with no harness detail beyond a produceWriteSkew helper and no third-party reproduction, which caps evidence below the level a corroborated or upstream-confirmed finding would earn.
Widely used libraries, retry still unbuilt
Adoption signals concern the surrounding ecosystem rather than the article's proposal. The libraries in question are broadly installed — typeorm-transactional is reported at 188,000 downloads a week — but the specific capability under discussion has no adoption at all: TypeORM issue #9806 has sat open since February 2023 with thirty reactions and no implementation, and typeorm-transactional ships isolation levels and seven propagation modes without retry. The author's whole-transaction-retry-plus-commit-hook pattern is demonstrated in illustrative code only, with no reported deployments, so measured adoption reflects a real installed base around an unimplemented feature.
Sound mechanics, slightly absolutist framing
Most of the writeup is measured and shows its work, and the headline mechanism is supported by quoted documentation and named error codes. The overstatement is narrow: 'unimplementable' and 'the only sound design' are absolute claims resting on one author's reasoning plus one self-run suite, and the version-matrix anecdote (four majors, one workload, no minor versions or plan detail) is presented as a general lesson about Postgres timing. Coverage also generalises the ecosystem's posture from two data points. That yields a mild positive gap rather than a substantial one.
Self-published author advocating own design
The single source is a self-published developer post whose author states he spent time building the requested feature and then argues that the requested form cannot be built while his alternative design is the sound one — a mild advocacy incentive, plus the ordinary reputational upside of a contrarian technical claim on a developer platform. Countervailing factors keep the score low: no vendor, sponsor, employer or commercial product is disclosed or promoted, the criticised projects are open source, and the argument is exposed via quoted documentation and runnable code that readers can check.
Confident on mechanism, thin on breadth
Confidence is split. The transaction-abort mechanism, the 25P02 masking effect and the commit-hook remedy are stated precisely enough to be trusted and independently checked, which supports moderate confidence. Against that, the cluster has one publisher, one author and one test suite; the empirical version finding is unreplicated; and there is no maintainer, upstream or competing-library input to confirm the ecosystem framing. That combination lands confidence just below the midpoint.
build
The optional EntityManager is the bug: moving the transaction boundary into AsyncLocalStorage1 distinct publisher
build
The NestJS default path puts the query inside the business rule, and nothing fails when it moves1 distinct publisher
build
A unique index is not a duplicate check: the race inside a webhook idempotency middleware1 distinct publisher
build
A GAN beauty filter is a device budget allocation, not a feature toggle1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 25, 2026