How to use database transactions in the repository pattern

I would like to know how to use the repository pattern with Prisma transactions.

I am using OAuth, so when the user authenticates, I need to populate the user, account and session tables. If any of these fail, I will need to rollback - that’s why the transaction. Each of these tables are in different repositories with their respective creation methods.

How can I use Transaction in this scenario and still be able to make it work in the in-memory repository without my use case knowing the transaction.

This is the code snippet that should be executed, with each create being in a repository - I put them all together to make it easier to understand the flow I want

await this.prismaService.$transaction(async (prisma) => {
        const newUser = await prisma.user.create({
          data: {
            email,
            username,
            name,
          },
        });

      await prisma.account.create({
        data: {
          user_id: newUser.id,
          provider: provider,
          provider_account_id: oauthUserId,
          access_token: accessToken,
          token_type: tokenType,
          type: 'oauth',
        },
      });

      await prisma.session.create({
        data: {
          user_id: newUser.id,
          expires: sessionExpires,
          session_token: sessionToken,
        },
      });
    });

Solution: Dependency Injection of the Prisma Client

Pass the Prisma client (prisma) into your repositories, but only internally — your use case doesn’t need to know about it.


Step-by-Step Implementation:

1. Repositories accept an optional Prisma client

// user.repository.ts
export class UserRepository {
  constructor(private prisma: PrismaClient) {}

  async createUser(data: CreateUserDto, tx?: PrismaClient) {
    const prisma = tx || this.prisma;
    return prisma.user.create({ data });
  }
}

Repeat this for AccountRepository and SessionRepository.


2. Your service handles the transaction, and passes tx into each repo

await this.prismaService.$transaction(async (tx) => {
  const user = await this.userRepository.createUser({ email, username, name }, tx);

  await this.accountRepository.createAccount({
    user_id: user.id,
    provider,
    provider_account_id: oauthUserId,
    access_token: accessToken,
    token_type: tokenType,
    type: 'oauth',
  }, tx);

  await this.sessionRepository.createSession({
    user_id: user.id,
    expires: sessionExpires,
    session_token: sessionToken,
  }, tx);
});

3. In-memory repository version just ignores the tx param

async createUser(data: CreateUserDto, tx?: any) {
  // ignore tx, do in-memory logic
  this.db.push(data);
  return data;
}

Benefits:

  • Use case stays clean — no knowledge of Prisma or transactions.
  • Repositories are testable — they accept a transaction object, but can work without it.
  • Rollback works as expected if any step fails.