Files
gh-djankies-claude-configs-…/skills/configuring-transaction-isolation/references/race-conditions.md
2025-11-29 18:22:25 +08:00

2.7 KiB

Preventing Race Conditions

Lost Update Problem

Scenario: Two transactions read the same value, both update it, one overwrites the other.

Without Isolation:

const product = await prisma.product.findUnique({
  where: { id: productId }
});

await prisma.product.update({
  where: { id: productId },
  data: { stock: product.stock - quantity }
});

Transaction A reads stock: 10 Transaction B reads stock: 10 Transaction A writes stock: 5 (10 - 5) Transaction B writes stock: 8 (10 - 2) Result: Stock is 8, but should be 3

With Serializable Isolation:

await prisma.$transaction(
  async (tx) => {
    const product = await tx.product.findUnique({
      where: { id: productId }
    });

    if (product.stock < quantity) {
      throw new Error('Insufficient stock');
    }

    await tx.product.update({
      where: { id: productId },
      data: { stock: { decrement: quantity } }
    });
  },
  {
    isolationLevel: Prisma.TransactionIsolationLevel.Serializable
  }
);

One transaction succeeds, the other gets P2034 and retries with fresh data.

Double-Booking Problem

Scenario: Two users try to book the same resource simultaneously.

Solution:

async function bookSeat(userId: string, seatId: string) {
  try {
    await prisma.$transaction(
      async (tx) => {
        const seat = await tx.seat.findUnique({
          where: { id: seatId }
        });

        if (seat.status !== 'AVAILABLE') {
          throw new Error('Seat no longer available');
        }

        await tx.seat.update({
          where: { id: seatId },
          data: {
            status: 'BOOKED',
            userId,
            bookedAt: new Date()
          }
        });
      },
      {
        isolationLevel: Prisma.TransactionIsolationLevel.Serializable
      }
    );

    return { success: true };
  } catch (error) {
    if (error.code === 'P2034') {
      throw new Error('Seat was just booked by another user');
    }
    throw error;
  }
}

Phantom Read Problem

Scenario: Query for rows matching a condition, insert happens, re-query shows different results.

Example with RepeatableRead:

await prisma.$transaction(
  async (tx) => {
    const activeUsers = await tx.user.findMany({
      where: { status: 'ACTIVE' }
    });

    const count = activeUsers.length;

    await tx.report.create({
      data: {
        type: 'USER_COUNT',
        value: count,
        timestamp: new Date()
      }
    });
  },
  {
    isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead
  }
);

RepeatableRead prevents other transactions from changing existing rows, but may still allow new inserts (phantom reads) depending on database implementation.