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 client

  • Buffers 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.

Previous
Previous

The Payload Was There All Along: Cross-Runtime Protobuf Is a Lie