Build1 distinct publisher3 min readUpdated
A dev.to write-up traces silent partial commits to a parameter nobody notices is missing, and replaces it with async context. The new boundary brings three fresh failure modes.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A dev.to write-up opens with a rollback that ran clean and left a row behind anyway, inside a NestJS application with a repository layer [1]. The cause was not TypeORM and not PostgreSQL: one repository was never inside the transaction, because the EntityManager stopped being passed down three layers up, with no exception and no warning, and the tests passed because that repository was mocked [2][3].
The mechanism is worth stating plainly, because it explains why the failure is silent. In TypeORM the manager is the transaction; a repository that does not use that manager issues its queries on a different connection, so those writes sit outside the transaction and the rollback does not revert them [4]. For a small project, the plain `dataSource.transaction(async (manager) => ...)` form is the correct answer and needs nothing added [5].
Once a repository layer exists, the manager has to travel, and the only way it travels is by hand [6]. The optional parameter then spreads upward into the use case and outward into the port itself: the `UserRepository` interface lives in the domain layer and every method grows a `manager?: EntityManager`, which means the domain now imports from `typeorm` [6][7]. At that point the port has stopped being a port, since it cannot be implemented without dragging the ORM along, including into the in-memory double you would otherwise use to test the application layer [8].
The correctness argument is the sharper one. That question mark carries the correctness of the system and is invisible: one missed argument, in one branch, of one service, is enough to put a write outside the transaction, and it passes code review and passes tests that mock the repository before surfacing in production as something like a user row with no matching settings row [9]. Dropping transactions instead resolves nothing; it postpones the problem to the moment two related writes diverge [10].
The proposed shape moves the boundary to a single point, the controller handling the request, and lets repositories enlist themselves in the transaction in progress without receiving anything as a parameter, in about sixty lines built on AsyncLocalStorage [11]. Node has shipped AsyncLocalStorage since v12: a value set at the root of an asynchronous call chain is readable at any depth, across every await, without being threaded through signatures [12]. The stated goals are a domain port with no TypeORM types, a boundary declared once, repositories that behave identically when no transaction is active, and forgetting to pass something no longer being a possible mistake because nothing is passed [17]. That last item is the real claim: the defect class from earlier is an omitted argument, and an omitted argument cannot exist where no argument does [18].
The second half is the part that matters operationally. Making the boundary implicit makes it wide, and the article names three consequences, each with a fix [16]. A network call inside the transaction holds a pooled connection and its locks for the entire wait [13]. A failure record written in the `catch` is rolled back along with the very failure it was meant to document [14]. And nesting two `execute` calls does not open a nested transaction but two independent ones, with the self-deadlock that permits [15].
Rank those before adopting anything. The catch-block one is the quietest: audit and failure rows written inside a boundary you no longer see in the signature disappear with the rollback, so incident evidence is exactly the data most likely to be lost [14]. The connection-holding one is a capacity problem that only appears under load [13], and the double-boundary deadlock is a self-inflicted stall that will look like a hang, not an error [15].
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.
In a NestJS application with a repository layer it is possible to run a rollback with no errors and then find a row still in the database that should have disappeared with it.
The described approach opens the transaction at a single point, the controller handling the request, and has repositories enlist themselves in the transaction in progress without receiving anything as a parameter; it comes to about sixty lines built on AsyncLocalStorage.
The cause was not a TypeORM or PostgreSQL bug: one of the repositories involved was never inside the transaction, because the EntityManager stopped being passed down three layers up.
There was no exception and no warning, and the tests passed because that repository was mocked.
The manager is the transaction: if a repository does not use that manager, its queries run on a different connection and end up outside the transaction, silently, with no error or warning, and the rollback does not revert them.
TypeORM offers await dataSource.transaction(async (manager) => { ... }), and for a small project this is the correct answer and nothing more is needed.
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.
Single-author reasoning with partial code, no external corroboration
The technical claims are internally coherent and illustrated with interface and class listings, and the mechanism (EntityManager identity determining which connection a query uses; AsyncLocalStorage scoping) is described precisely. But every claim traces to one dev.to article, duplicated verbatim as a second item on the same platform, so there is no independent publisher, reproduction, benchmark, or issue tracker reference. The supplied body is also truncated mid-listing and before the promised fixes for the three consequences, so the design cannot be inspected end to end.
No adoption signal in supplied sources
The sources contain no release, deployment, benchmark, usage disclosure, download figure, repository link, or third-party account of anyone using the pattern. The only usage referenced is the author's unnamed project, described anecdotally. No adoption value can be derived without inventing facts.
Mildly overstated by the 'impossible' framing, tempered by the author's own caveats
The headline promise — making a failure class impossible — is a strong absolute resting on one engineer's unvalidated design, and the elimination claim is true only for the narrow defect of a forgotten argument. Against that, the article is unusually candid: it says the built-in dataSource.transaction is sufficient for small projects and devotes its second half to three failure modes the new boundary introduces. Net overstatement is therefore small rather than large.
Personal-platform authorship and duplicate posting; no disclosed commercial stake
This is self-published developer-platform content advocating the author's own pattern, which carries a reputational and audience-building incentive, amplified by the same article appearing twice on dev.to under different member accounts within roughly half an hour. No vendor sponsorship, product, paid course, or commercial relationship with NestJS, TypeORM, or any provider is disclosed or implied in the supplied text, so the incentive pressure is modest rather than strong.
Mechanism plausible and well specified; validation and adoption absent
Confidence is moderate-low. The described mechanism is standard and checkable in principle, and the article's self-imposed caveats increase credibility. But the cluster is one publisher, one text, duplicated; the implementation is truncated; there is no adoption or measurement evidence; and the operational consequences of a longer-lived transaction boundary are asserted rather than measured.
build
The NestJS default path puts the query inside the business rule, and nothing fails when it moves1 distinct publisher
build
1,254 dead mutants, a 100% score, and a payment charged twice1 distinct publisher
build
A GAN beauty filter is a device budget allocation, not a feature toggle1 distinct publisher
build
Every viewer hits your HLS key endpoint in the same second, and almost nobody tests it1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
2 articles · August 18, 2026