Prisma Didn’t Give Me a Unit of Work, So I Gave It Arson
How I added scoped transactional consistency to a Prisma stack, replaced NestJS with explicit architecture, and survived to blog about it.
The Problem: Prisma Doesn't Care About Your Feelings
Let me set the scene.
You're working in a codebase where every command handler touches the database, fires off some events, and maybe triggers more commands. The architecture looks clean, but if any piece of the chain fails, you get partial writes, corrupted state, and a very spicy page to your incident channel.
Prisma, in all its type-safe sugar-coated glory, has one glaring weakness:
It doesn't support ambient transactions.
You want a Unit of Work. You want to say, "everything that happens in this chain either fully commits or it burns to the ground." Prisma says:
"Just pass the
tx
manually. Everywhere. Forever."
So I built my own system. And then I burned the rest.
Step 1: Nuking NestJS
The original codebase was NestJS. Decorators everywhere. Logic hidden behind annotations. Security bugs no one could see because the access control lived in some forgotten metadata layer.
So I did what any reasonable engineer would do.
I burned it down.
We rebuilt in Express. Explicit routing. No decorators. Just a clean command bus, some middleware, and handlers that actually showed you what was happening.
Step 2: Building the ExecutionContext
If Prisma won’t give us scoped transaction context, we have to make our own.
So I created ExecutionContext
— an object that:
Holds the active Prisma
tx
clientBuffers domain events
Stores result data (like newly created entity IDs)
Gets passed to every command, event, and repo call
But passing context everywhere sucks. So...
Step 3: AsyncLocalStorage to the Rescue
We used AsyncLocalStorage
(from Node.js core) to create an ambient context that lives per async call chain.
Now we can call getContext()
anywhere inside a handler or repo, and it Just Works™.
Unless you're in a unit test. Then it Definitely Doesn't Work™.
Step 4: Fallbacks and Test Sanity
To keep legacy tests alive, getContext()
now returns a fallback ExecutionContext
with a warning.
This prevents test explosions while you migrate code over to the new system. And when someone forgets to set context in prod?
They'll get a crash. Because prod doesn't get safety rails.
The Results
Full transactional consistency across command + event chains
No more partial writes or ghost events
Explicit architecture that even a junior dev can read
An architecture I can sleep next to
Also: memes.
TL;DR
Prisma didn’t give me a Unit of Work, so I gave it arson. And now I have:
Scoped transactions via ExecutionContext
Event buffering with rollback safety
A CQRS pipeline that isn’t made of lies
And yes, I replaced decorators with dogs.
About the Author: Glenn Eggleton is a Principal-level systems architect in disguise. He builds scalable, event-safe infrastructure with caffeine, memes, and an unshakable sense of architectural justice.