🔒 Concurrency Control & Transaction Management

Deep Dive into Race Condition Handling in the Codebase

🎯 Our Strategy: Entity-Based Pessimistic Locking

The codebase employs a **pessimistic, entity-based locking approach** tailored for **Google Datastore's eventually-consistent nature** to protect critical operations like payment and allocation.

Key Observations

  • No @Version fields: Does not use JPA optimistic locking.
  • Deliberate Single Retry: Uses transactNew(1, work) for **controlled retry behavior** (not for application conflict resolution).
  • Entity-Based Locks: Locks are persisted entities with a **10-minute expiration** to prevent deadlocks.
  • Graceful Degradation: Different strategies for handling contention based on context (e.g., ignoring for Gate Access, throwing for Payment Processing).

⚙️ Transaction Management

Centralized TransactionProvider (src/main/java/nz/co/parkable/dal/TransactionProvider.java)
  • transactWithReturn(): Reuses existing transaction or creates a new one.
  • transact(): Non-returning version.
  • tryOnceWithReturn(): Creates new transaction with **single retry** (transactNew(1, work)).
  • transactionless(): For read-heavy operations outside transactions.

Critical Transaction Areas

🛡️ Three-Layer Pessimistic Locking Implementation

1. Application-Level Locks (User/Service Scope)

Used for user-level operations (e.g., preventing duplicate parking requests).

// LockService.java - Creates an ObjectLock entity
try (LockCloseable ignored = objectLockService.createLock(lockKey)) {
    // Protected operation
}
// Resource Locking Pattern (Fail Fast)
try (LockCloseable ignored = lockService.createLock(id)) {
    // update logic
} catch (LockException e) {
    throw new BadRequestException("This is being updated. Please wait...");
}
2. Entity-Specific Locks (Resource Scope)

Used for resources like parking bays to prevent double-booking (Lock Before Allocate).

// ParkingRequestLockService.java: Bay Locking
transactionProvider.transact(() -> {
    if (existingRequestLock != null) {
        throw new BayInUseException(); // Fail immediately
    }
    parkingRequestLockRepo.save(lock);
});
3. Concurrent Modification Detection

Low-level check for Datastore aborts (Code.ABORTED or "transaction closed").

// ExceptionUtils.java
public static boolean isConcurrentModificationException(DatastoreException ex) {
    return Code.ABORTED.getNumber() == ex.getCode()
        || (Code.INVALID_ARGUMENT.getNumber() == ex.getCode()
            && ex.getMessage().contains("transaction closed"));
}

**Usage:** Silently ignored for Gate Access; Throws exception for Payment Processing.

⚖️ Concurrency Control Approaches

Our strategy is **Application-Level Locking**. Below is a comparison with key alternatives.

Approach Contention Mechanism/Example Best Use Case ✅ Disadvantages ❌
Optimistic Low Uses @Version field; checks at commit time. User Profiles, CMS, Read-heavy workloads. Fails late, requires application retry logic, wasted work.
Pessimistic (RDBMS) High SELECT... FOR UPDATE (Acquire exclusive lock immediately). Financial transactions, Inventory, Short transactions. Reduces concurrency, can cause deadlocks, performance bottleneck.
Application Lock (Our Codebase) Medium Creating lock entities: if (lockRepo.exists(id)) throw; lockRepo.save(lock); **Google Datastore / NoSQL**, Cross-service coordination, Custom timeout logic. Medium complexity.
MVCC Any Database maintains multiple versions (PostgreSQL, Oracle). General RDBMS. None (automatic with proper setup).

💡 Future Considerations: Hybrid Approach

The current pessimistic strategy is excellent for high-value operations. For future optimization, a **Hybrid Approach** could be considered:

  • Optimistic: Apply **@Version** locking to **low-contention entities** (e.g., User settings, Organization configs).
  • Pessimistic: Keep the current entity-based locks for **write-heavy critical paths** (Payment, Bay Allocation, Gate Access).