The main problem I had when finding a suitable solution was, that the prisma client in the lambda of an interactive transaction is not a fully fledged Client, but just Prisma.TransactionClient, which is missing connect, $disconnect, transaction methods. If prisma would provide a full Client at this place, all you could do to solve the problem was just doing transactions like this:

**THIS DOES NOT WORK BECAUSE prismaServiceBoundToTransaction IS JUST OF TYPE Prisma.TransactionClient!!!**
return await this.prismaService.$transaction(async (prismaServiceBoundToTransaction): Promise<any> => {
      const userService = new UserService(prismaServiceBoundToTransaction)
      const otherService = new OtherService(prismaServiceBoundToTransaction)
      
      //Following calls will use prismaServiceBoundToTransaction internally
      await userService.update()
      await otherService.delete()
    }

Of course above only works, if UserService and OtherService are stateless.

So for my solution I created a new Interface that will offer all methods of Prisma.TransactionClient, but also a custom method to create a transaction. All of the services like your UserService will only retrieve this exact interface, so they can't call $transaction but only my interactiveTransaction method!

export interface PrismaClientWithCustomTransaction
  extends Readonly<Prisma.TransactionClient> {
  interactiveTransaction<F>(
    fn: (prisma: Prisma.TransactionClient) => Promise<F>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    }
  ): Promise<F>;
}

We then create a concrete class TransactionalPrismaClient that implements the mentioned interface and delivers it, by retrieving a Prisma.TransactionClient in it's constructor and forwarding all of its methods. Additionally we also just implement the interactiveTransaction method by executing the lambda method with the Prisma.TransactionClient

export class TransactionalPrismaClient<
  T extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
  U = 'log' extends keyof T
    ? T['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
      ? Prisma.GetEvents<T['log']>
      : never
    : never,
  GlobalReject extends
    | Prisma.RejectOnNotFound
    | Prisma.RejectPerOperation
    | false
    | undefined = 'rejectOnNotFound' extends keyof T
    ? T['rejectOnNotFound']
    : false
> implements PrismaClientWithCustomTransaction
{
  constructor(private readonly transactionalClient: Prisma.TransactionClient) {}

  $executeRaw<T = unknown>(
    query: TemplateStringsArray | Prisma.Sql,
    ...values: any[]
  ): PrismaPromise<number> {
    return this.transactionalClient.$executeRaw(query, ...values);
  }
  $executeRawUnsafe<T = unknown>(
    query: string,
    ...values: any[]
  ): PrismaPromise<number> {
    return this.transactionalClient.$executeRawUnsafe(query, ...values);
  }
  $queryRaw<T = unknown>(
    query: TemplateStringsArray | Prisma.Sql,
    ...values: any[]
  ): PrismaPromise<T> {
    return this.transactionalClient.$queryRaw(query, ...values);
  }
  $queryRawUnsafe<T = unknown>(
    query: string,
    ...values: any[]
  ): PrismaPromise<T> {
    return this.transactionalClient.$queryRawUnsafe(query, ...values);
  }
  get otherEntity(): Prisma.OtherEntityDelegate<GlobalReject> {
    return this.transactionalClient.otherEntity;
  }
  get userEntity(): Prisma.UserEntityDelegate<GlobalReject> {
    return this.transactionalClient.userEntity;
  }

  async interactiveTransaction<F>(
    fn: (prisma: Prisma.TransactionClient) => Promise<F>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    }
  ): Promise<F> {
    return await fn(this.transactionalClient);
  }
}

And in your PrismaService we also need to implement the interactiveTransaction method, so that it satifies our defined interface PrismaClientWithCustomTransaction.

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, PrismaClientWithCustomTransaction
{
  private readonly logger = new ConsoleLogger(PrismaService.name);

  async onModuleInit() {
    this.logger.log('Trying to connect to db.');
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }

  interactiveTransaction<R>(
    fn: (prisma: Prisma.TransactionClient) => Promise<R>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    },
    numRetries = 1
  ): Promise<R> {
    let result: Promise<R> | null = null;

    for (let i = 0; i < numRetries; i++) {
      try {
        result = this.$transaction(fn, options);
      } catch (e) {
        if (e instanceof Prisma.PrismaClientKnownRequestError) {
          //TODO?
        } else {
          throw e;
        }
      }

      if (result != null) {
        return result;
      }
    }

    throw new Error(
      'No result in transaction after maximum number of retries.'
    );
  }
}

Because in our services we now expect the PrismaClientWithCustomTransaction interface, the auto injecting of NestJs wont work anymore and we have to provide PrismaService using a token:

providers: [
    {
      provide: 'PRISMA_SERVICE_TOKEN',
      useClass: PrismaService,
    },
  ],
exportt class UserService{
    constructor(@Inject('PRISMA_SERVICE_TOKEN') private readonly prisma: PrismaClientWithCustomTransaction){}
}

Alright so now we can do the following:

@Injectable()
export class OrdersService {
  constructor( @Inject('PRISMA_SERVICE_TOKEN')
    private readonly prisma: PrismaClientWithCustomTransaction, ...) {}

  async someFn() {
    return await this.prisma.interactiveTransaction(
      async (client) => {
        //You can still use client directly, if you dont need nested transaction logic
        return client.userEntity.create(...)

 
        //Or create services for nested usage
        const transactionalClient = new TransactionalPrismaClient(client);
        const userService = new UserService(transactionalClient);
        return userService.createUser(...);
        });
      },
      { isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead }
    );
  }
}

And if you need the connect, $disconnect, $use, you can of course still inject the original PrismaService with its regular interface.

Answer from Peter Herbst on Stack Overflow
🌐
Prisma
prisma.io › nestjs
Enterprise-ready database for NestJS apps - Prisma ORM
Prisma's transaction API integrates smoothly with NestJS's asynchronous architecture, allowing you to perform multiple database operations as a single atomic unit while following NestJS's best practices.
🌐
Medium
medium.com › @jedirichang › nestjs-prisma-transaction-with-repositories-2803e5a5487a
NestJS: Prisma Transaction with Repositories | by Richang Sharma | Medium
September 6, 2024 - Now Prisma on its own does provide transaction support, but doing transactions inside a request where there are multiple service methods to be executed in conjunction as a transaction is not at all suitable in Prisma for some reason. While researching I found this can be solved by using AsyncLocalStorage, to somehow carry over the transaction context throughout the callback/function stack lifecycle. And the best way to do it was the NestJS-CLS.
Discussions

How do you apply transactions?
I don’t think I understand what you’re trying to do or why. Using a global inceptor to commit/rollback transactions feels dirty. It’s unnecessarily opaque and confusing. Honestly the data layer has sufficient levels of complexity that I would probably keep it as simple as possible. More on reddit.com
🌐 r/nestjs
10
9
April 29, 2025
NestJs Prisma and Graphql transactions not recorded as intended
Environment SaaS (https://sentry.io/) Steps to Reproduce Install graphql and prisma to nest js project Run requests and do db queries Check transactions in Sentry import { NotFoundException, Unauth... More on github.com
🌐 github.com
11
June 16, 2024
Top answer
1 of 1
1

The main problem I had when finding a suitable solution was, that the prisma client in the lambda of an interactive transaction is not a fully fledged Client, but just Prisma.TransactionClient, which is missing connect, $disconnect, transaction methods. If prisma would provide a full Client at this place, all you could do to solve the problem was just doing transactions like this:

**THIS DOES NOT WORK BECAUSE prismaServiceBoundToTransaction IS JUST OF TYPE Prisma.TransactionClient!!!**
return await this.prismaService.$transaction(async (prismaServiceBoundToTransaction): Promise<any> => {
      const userService = new UserService(prismaServiceBoundToTransaction)
      const otherService = new OtherService(prismaServiceBoundToTransaction)
      
      //Following calls will use prismaServiceBoundToTransaction internally
      await userService.update()
      await otherService.delete()
    }

Of course above only works, if UserService and OtherService are stateless.

So for my solution I created a new Interface that will offer all methods of Prisma.TransactionClient, but also a custom method to create a transaction. All of the services like your UserService will only retrieve this exact interface, so they can't call $transaction but only my interactiveTransaction method!

export interface PrismaClientWithCustomTransaction
  extends Readonly<Prisma.TransactionClient> {
  interactiveTransaction<F>(
    fn: (prisma: Prisma.TransactionClient) => Promise<F>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    }
  ): Promise<F>;
}

We then create a concrete class TransactionalPrismaClient that implements the mentioned interface and delivers it, by retrieving a Prisma.TransactionClient in it's constructor and forwarding all of its methods. Additionally we also just implement the interactiveTransaction method by executing the lambda method with the Prisma.TransactionClient

export class TransactionalPrismaClient<
  T extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
  U = 'log' extends keyof T
    ? T['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
      ? Prisma.GetEvents<T['log']>
      : never
    : never,
  GlobalReject extends
    | Prisma.RejectOnNotFound
    | Prisma.RejectPerOperation
    | false
    | undefined = 'rejectOnNotFound' extends keyof T
    ? T['rejectOnNotFound']
    : false
> implements PrismaClientWithCustomTransaction
{
  constructor(private readonly transactionalClient: Prisma.TransactionClient) {}

  $executeRaw<T = unknown>(
    query: TemplateStringsArray | Prisma.Sql,
    ...values: any[]
  ): PrismaPromise<number> {
    return this.transactionalClient.$executeRaw(query, ...values);
  }
  $executeRawUnsafe<T = unknown>(
    query: string,
    ...values: any[]
  ): PrismaPromise<number> {
    return this.transactionalClient.$executeRawUnsafe(query, ...values);
  }
  $queryRaw<T = unknown>(
    query: TemplateStringsArray | Prisma.Sql,
    ...values: any[]
  ): PrismaPromise<T> {
    return this.transactionalClient.$queryRaw(query, ...values);
  }
  $queryRawUnsafe<T = unknown>(
    query: string,
    ...values: any[]
  ): PrismaPromise<T> {
    return this.transactionalClient.$queryRawUnsafe(query, ...values);
  }
  get otherEntity(): Prisma.OtherEntityDelegate<GlobalReject> {
    return this.transactionalClient.otherEntity;
  }
  get userEntity(): Prisma.UserEntityDelegate<GlobalReject> {
    return this.transactionalClient.userEntity;
  }

  async interactiveTransaction<F>(
    fn: (prisma: Prisma.TransactionClient) => Promise<F>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    }
  ): Promise<F> {
    return await fn(this.transactionalClient);
  }
}

And in your PrismaService we also need to implement the interactiveTransaction method, so that it satifies our defined interface PrismaClientWithCustomTransaction.

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, PrismaClientWithCustomTransaction
{
  private readonly logger = new ConsoleLogger(PrismaService.name);

  async onModuleInit() {
    this.logger.log('Trying to connect to db.');
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }

  interactiveTransaction<R>(
    fn: (prisma: Prisma.TransactionClient) => Promise<R>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    },
    numRetries = 1
  ): Promise<R> {
    let result: Promise<R> | null = null;

    for (let i = 0; i < numRetries; i++) {
      try {
        result = this.$transaction(fn, options);
      } catch (e) {
        if (e instanceof Prisma.PrismaClientKnownRequestError) {
          //TODO?
        } else {
          throw e;
        }
      }

      if (result != null) {
        return result;
      }
    }

    throw new Error(
      'No result in transaction after maximum number of retries.'
    );
  }
}

Because in our services we now expect the PrismaClientWithCustomTransaction interface, the auto injecting of NestJs wont work anymore and we have to provide PrismaService using a token:

providers: [
    {
      provide: 'PRISMA_SERVICE_TOKEN',
      useClass: PrismaService,
    },
  ],
exportt class UserService{
    constructor(@Inject('PRISMA_SERVICE_TOKEN') private readonly prisma: PrismaClientWithCustomTransaction){}
}

Alright so now we can do the following:

@Injectable()
export class OrdersService {
  constructor( @Inject('PRISMA_SERVICE_TOKEN')
    private readonly prisma: PrismaClientWithCustomTransaction, ...) {}

  async someFn() {
    return await this.prisma.interactiveTransaction(
      async (client) => {
        //You can still use client directly, if you dont need nested transaction logic
        return client.userEntity.create(...)

 
        //Or create services for nested usage
        const transactionalClient = new TransactionalPrismaClient(client);
        const userService = new UserService(transactionalClient);
        return userService.createUser(...);
        });
      },
      { isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead }
    );
  }
}

And if you need the connect, $disconnect, $use, you can of course still inject the original PrismaService with its regular interface.

🌐
Wanago
wanago.io › home › api with nestjs #104. writing transactions with prisma
API with NestJS #104. Writing transactions with Prisma
April 17, 2023 - In this article, we’ve discussed the idea of transactions and how to use them with Prisma. When doing that, we’ve compared various solutions, such as nested writes, bulk operations, and the transactions API. We’ve also used both the sequential operations approach and the interactive transactions. All of the above equips us with solutions for many different use cases we might encounter in our applications. Series Navigation<< API with NestJS #103.
🌐
Medium
masoudx.medium.com › stop-passing-the-hot-potato-managing-prisma-transactions-in-nestjs-8f30eeb5bb54
Stop Passing the Hot Potato: Managing Prisma Transactions in NestJS | by Masoud Banimahd | Medium
November 25, 2025 - A transaction ensures that these three steps are treated as a single atomic unit. Either they all happen, or none of them happen. If step 3 fails, steps 1 and 2 roll back as if they never touched the database. In NestJS with Prisma, handling these transactions is straightforward when everything happens in one function.
🌐
Medium
medium.com › @yisarasaq2018 › database-transaction-in-prisma-with-nestjs-and-postgres-a4bb4bbb6426
DATABASE TRANSACTION IN PRISMA WITH NESTJS AND POSTGRES | by Yisa Rasaq | Medium
January 14, 2025 - Using Prisma’s $transaction method in a NestJS application allows you to group multiple database operations into a single transaction.
Find elsewhere
🌐
GitHub
github.com › takeny1998 › nestjs-prisma-transactional
GitHub - takeny1998/nestjs-prisma-transactional: Manage PrismaORM transactions easily by adding decorator to your nest.js services. · GitHub
Manage PrismaORM transactions easily by adding decorator to your nest.js services. ... Import the TransactionMoudle to AppModule of your nest.js project. Or just add it to the module you want to use. import { TransactionModule } from ...
Author   takeny1998
🌐
Callgent
docs.callgent.com › blog › nestjs+prisma-transaction-propagation-and-test-rollback-and-tenancy
🔧 Nestjs + Prisma, transaction propagation & test rollback & multi-tenancy - Service as a Callgent
March 16, 2024 - After some investigation, we come to @nestjs-cls/transactional, @Injectable() export class MyService { constructor( private readonly txHost: TransactionHost<TransactionalAdapterPrisma>, private readonly anotherService: AnotherService, ) {} @Transactional() async myMethod(){ const prisma = this.txHost.tx as PrismaClient; // db operations1 await this.anotherService.anotherMethod(); // db operations2 // db operations3 } }
🌐
DEV Community
dev.to › callgent › nestjs-prisma-transaction-propagation-test-rollback-multi-tenancy-4485
Nestjs + Prisma, transaction propagation & test rollback & multi-tenancy - DEV Community
June 23, 2024 - After some investigation, we come to @nestjs-cls/transactional, @Injectable() export class MyService { constructor( private readonly txHost: TransactionHost<TransactionalAdapterPrisma>, private readonly anotherService: AnotherService, ) {} @Transactional() async myMethod(){ const prisma = this.txHost.tx as PrismaClient; // db operations1 await this.anotherService.anotherMethod(); // db operations2 // db operations3 } }
🌐
Papooch
papooch.github.io › plugins › available plugins › @nestjs-cls/transactional › prisma adapter
Prisma adapter | NestJS CLS - Ing. Ondřej Švanda
Since Prisma does not yet support nested transactions natively, the adapter implements a custom solution via raw queries and SQL SAVEPOINTS.
🌐
npm
npmjs.com › package › @nestjs-cls › transactional-adapter-prisma
nestjs-cls/transactional-adapter-prisma
Prisma adapter for the @nestjs-cls/transactional plugin.
      » npm install @nestjs-cls/transactional-adapter-prisma
    
Published   Dec 15, 2025
Version   1.3.2
🌐
GitHub
github.com › nicheeeer › prisma-transaction
GitHub - nicheeeer/prisma-transaction: prisma-transaction is a library that provides a simple way to use transactions in Prisma within NestJS using decorators · GitHub
prisma-transaction is a library that provides a simple way to use transactions in Prisma within NestJS using decorators - nicheeeer/prisma-transaction
Author   nicheeeer
🌐
Reddit
reddit.com › r › node › comments › 12pvair › api_with_nestjs_104_writing_transactions_with
API with NestJS #104. Writing transactions with Prisma
Unofficial Node.js subreddit. Discuss Node.js, JavaScript, TypeScript, and anything else in the Node.js ecosystem!
🌐
Papooch
papooch.github.io › plugins › available plugins › @nestjs-cls/transactional
@nestjs-cls/transactional | NestJS CLS
The plugin works in conjunction with various adapters that provide the actual transactional logic and types for the underlying database library, so you'll need to install one of those as well. Adapters for the following libraries are available: Prisma (see @nestjs-cls/transactional-adapter-prisma)
🌐
npm
npmjs.com › package › @myfunc › prisma-transactional
@myfunc/prisma-transactional - npm
June 27, 2024 - As an idea - implement ESLint rule for nested prisma queries that might be unintentionally executed in transaction. That means a developer will be aknowledged about possible transaction wrapping and force him to add an eslint-ignore comment. [ ] Clean code. prisma · @prisma/client · nest · nestjs ·
      » npm install @myfunc/prisma-transactional
    
Published   Jun 27, 2024
Version   0.3.0
Author   myfunc
🌐
Callgent's Blog
blog.callgent.com › nestjs-prisma-transaction-propagation-test-rollback-multi-tenancy
Nestjs + Prisma, transaction propagation & test rollback & multi-tenancy
June 27, 2024 - After some investigation, we come to @nestjs-cls/transactional, @Injectable() export class MyService { constructor( private readonly txHost: TransactionHost<TransactionalAdapterPrisma>, private readonly anotherService: AnotherService, ) {} @Transactional() async myMethod(){ const prisma = this.txHost.tx as PrismaClient; // db operations1 await this.anotherService.anotherMethod(); // db operations2 // db operations3 } }
🌐
GitHub
github.com › prisma › prisma › discussions › 23626
i am facing the same issue in nest js when a transaction fail it dosent roll back the changes in the database · prisma/prisma · Discussion #23626
March 26, 2024 - Can you also try a different Transaction Isolation Level and check if you get the same error? Beta Was this translation helpful? Give feedback. ... There was an error while loading. Please reload this page. Something went wrong. There was an error while loading. Please reload this page. ... `public removeAssistant = async (doctor_id: string): Promise<ServiceResponse<string | null>> => { const doctor = await prisma.doctor.findUnique({where:{id: doctor_id}}); if (!doctor?.assistant_id) { return new ServiceResponse(null, true, httpStatus.BAD_REQUEST, 'Assistant is not assigned to doctor'); }
Author   prisma
🌐
GitHub
github.com › getsentry › sentry-javascript › issues › 12530
NestJs Prisma and Graphql transactions not recorded as intended · Issue #12530 · getsentry/sentry-javascript
June 16, 2024 - Each graphql request is reported as seperate transaction. Prisma queries are reported and visible in dashboard.
Author   getsentry
🌐
Simon Hayden
simon.hayden.wien › blogs › nestjs + prisma + proxy = ♥️
Nestjs + Prisma + Proxy = ♥️ | Simon Hayden
February 15, 2025 - More precisely, the is this function ... of a transaction code . Something that’s quite easy to forget. And if we ever need to update this code, we have to update every location that interacts with Prisma. We can fix this with factory methods or even TypeScript decorators. But we are here for NestJS magic, so ...