VolverBack

02 — ARTÍCULO_ARTICLE_

Laboratorio 1 - Concurrent Booking SystemLaboratorio 1 - Concurrent Booking System

SEP 2026SEP 20266 MINMINVer repositorioView repository ↗
02SEP 2026SEP 2026

▦

A ticket booking system for high-concurrency events. The challenge: preventing overbooking when 200 users try to buy at the same time. I evaluated three concurrency-control strategies with Spring Boot and PostgreSQL, measuring the p50 and p95 percentiles with Locust.

The experiment lives in three branches: main with atomic validation, pessimistic-lab1 with pessimistic locking, and optimistic-lab1 with optimistic locking.

Branches

BranchStrategyDescription
mainAtomic validationA single UPDATE with WHERE availableStock > 0. No locks, no prior reads. The best approach.
pessimistic-lab1Pessimistic lockingfindById with @Lock(PESSIMISTIC_WRITE) before decrementing. Locks the row for the whole transaction.
optimistic-lab1Optimistic lockingfindById with @Version on the entity. On conflict it throws OptimisticLockException and the client must retry.

Problem: unprotected in-memory validation

When stock is read, validated in memory, and then updated with no concurrency mechanism, the concurrent flow lets multiple requests read the same stock before anyone writes. This causes overbooking since there is no protection at the database level.

Race conditionjava
// Problemático - sin protección atómica
Optional<Event> optionalEvent = eventRepository.findById(dto.eventId());
Event event = optionalEvent.get();
if(event.getAvailableStock() <= 0) return -1L;
event.setAvailableStock(event.getAvailableStock() - 1); // Race condition
bookingRepository.save(booking);

Solutions

Pessimistic Locking (pessimistic-lab1)

It locks the row in the database with SELECT ... FOR UPDATE before reading. The problem is that it turns the concurrent flow sequential: every request waits until the transaction finishes, raising the 50th and 95th percentiles.

Pessimistic Lockingjava
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT e FROM Event e WHERE id = :id")
Optional<Event> findByIdWithPessimisticLock(@Param("id") long id);

Optimistic Locking (optimistic-lab1)

It uses a @Version field on the entity. It does not lock the row; the flow stays concurrent. If two transactions try to modify the same record, the second one throws OptimisticLockException and must retry. Better performance than pessimistic but with retry overhead.

Optimistic Lockingjava
@Entity
public class Event {
    @Id
    private Long id;

    @Column(name = "available_stock")
    private Long availableStock;

    @Version
    private Integer version;
}

Atomic Validation (main)

A single UPDATE query with a WHERE availableStock > 0 condition. It locks no rows, no reads, no retries. The best approach for this use case.

Atomic Validationjava
@Modifying
@Query("""
    UPDATE Event SET availableStock = availableStock - 1
    WHERE id = :id AND availableStock > 0
""")
int actualizarEspaciosDisponibles(@Param("id") Long id);

Benchmarks (Locust - 200 concurrent users)

Optimistic Locking

  • p50: ~30ms | p95: ~45ms
  • It does not lock the row; the flow stays concurrent. Performance is better than pessimistic but the number of retries a user must perform to buy a ticket grows considerably.
Locust results for optimistic locking with 200 concurrent users
Results: optimistic locking.

Pessimistic Locking

  • p50: ~81ms | p95: ~140ms
  • It turns the concurrent flow sequential. The row stays locked for the whole transaction and requests wait until it finishes, notably raising the 50th and 95th percentiles.
Locust results for pessimistic locking with 200 concurrent users
Results: pessimistic locking.

Atomic Validation (best approach)

  • p50: ~21ms | p95: ~33ms
  • The row is only locked for milliseconds during the UPDATE. No retries, no overbooking. The best performance of the three approaches.
Locust results for atomic validation with 200 concurrent users
Results: atomic validation.

Stack

  • Java 25 / Spring Boot
  • PostgreSQL
  • Locust ( benchmarks )

Lessons learned

  • Using Locust for REST API tests with 200 concurrent users and identifying p50 and p95 behavior across scenarios.
  • Applying LockModeType in transactions for concurrency problems and verifying how one or the other affects performance.

VolverBack