Tripping on ACID | Scot Murphy
Tripping on ACID
Notes on cross-boundary transaction hijacking
What I noticed about commit ownership drifting across packages, triggers, scripts, and clients — in one horribly complex system I help maintain
Let’s start with the good news: your database is (probably) not on drugs.
The bad news? Its transaction boundaries might be absolutely tripping. Mine were.
I could not answer a simple operational question about systems I work on every week: who decides when this operation becomes durable? Not which service owns the API. Not which team owns the package. Who issues the COMMIT that makes the state real?
I am not writing this as the authority on those systems. I am the person who got tired of not knowing. After enough half-done data and residual side effects, that question became a private obsession — a hobby I did not consent to. What I kept finding was a small set of ways our stack could hallucinate who owns the transaction : partial-persistence loops, rollback-then-commit exception flows, autonomous triggers that survive rollbacks like cockroaches in a fallout shelter. Still in production. Still ruining Mondays.
What this is: notes from grepping and incident-chasing one multi-tier estate — costs I saw, shapes in the code, questions that helped me stop guessing.
What this is not: a universal diagnosis, “delete every COMMIT,” a claim every shape is always wrong, or a finished fix story. Some patterns are intentional policy. Most of what bit us was undocumented ownership, not the keyword itself. Your estate may rhyme; it will not be identical.
The jokes are free. The residual rows are not.
Patterns below are from real code paths I touched. Comedy is a coping mechanism, not a severity rating — and not a claim that I have this solved.
Shapes come from a large Oracle PL/SQL + fat-client desktop + ASP.NET mess. If you have more than one layer that can finalize work, you may recognize the shape even if the dialect differs.
When durability becomes a suggestion
I can usually answer who owns a service endpoint. I often could not answer who owns COMMIT on a multi-layer write path.
When ownership is ambiguous, independent control points fire on one logical operation: a desktop client with COMMIT USING, an app repository that never opens a TransactionScope, a package body that commits mid-workflow, an autonomous trigger with its own durability channel, a release script that decides rollback policy on the way out. None of that was exotic in our tree. Each piece was locally rational. Together, durability stopped feeling like a designed boundary and started feeling like an emergent outcome of whichever layers ran — often undocumented, occasionally contradictory, invisible until something broke. COMMIT felt less like an authoritative command and more… like a suggestion.
Working label I ended up using — transaction boundary hijacking : any layer that changes durability outcome outside a clear ownership contract — commits it did not own as the boundary, side effects that outlive upstream rollback, atomic units sliced by hidden intermediate commits, or durability coupled to object mechanics (ON COMMIT refresh) instead of policy. The cases that confused me were not “someone used COMMIT.” They were “someone used COMMIT where it rewrote the contract I thought the caller had.”
The framing that helped: a transaction boundary is control-plane machinery. When multiple layers can override it unilaterally, atomicity and recoverability stop behaving like guarantees and start behaving like path accidents. ACID properties stop reading like properties. They start reading like vibes.
How the objects lined up in our stack — call sites and in-database things, each able to finalize something:
flowchart TB<br>subgraph outside [Outside the database]<br>direction LR<br>Desktop([Desktop clientwindow + connection handle])<br>App[Web MVC apprepository class]<br>Scripts[/Release or backoutSQL runner scripts/]<br>end
subgraph inside [Inside the database]<br>direction TB<br>Pkg[[Package bodyworkflow_pkg]]<br>Trig{{Triggerautonomous transaction}}<br>Biz[(Business tablesorders, balances)]<br>Side[(Side-effect tablesaudit, error_log)]<br>Chunk[(Work or staging tableschunk N of M durable)]<br>end
Desktop -->|COMMIT USING / ROLLBACK USING| Pkg<br>App -->|ExecuteProc, often no TransactionScope| Pkg<br>Scripts -->|script-level COMMIT / ROLLBACK| Pkg
Pkg -->|COMMIT primary path| Biz<br>Pkg -->|fires| Trig<br>Trig -->|own COMMIT, survives caller ROLLBACK| Side<br>Pkg -->|COMMIT inside LOOP or FORALL| Chunk
classDef major fill:#0b1220,stroke:#64748b,color:#e2e8f0<br>classDef majorBiz fill:#052e16,stroke:#4ade80,color:#ecfdf5<br>classDef minor fill:#e2e8f0,stroke:#64748b,color:#0f172a<br>classDef minorWarn fill:#ffedd5,stroke:#ea580c,color:#7c2d12<br>classDef minorLeak fill:#fee2e2,stroke:#dc2626,color:#7f1d1d<br>class Pkg major<br>class Biz majorBiz<br>class Desktop,App,Scripts minor<br>class Trig minorWarn<br>class Side,Chunk minorLeak<br>style outside...