Handling concurrency issues in Kotlin + Spring Boot REST APIs

Concurrency issues occur when multiple threads or requests access and modify the same resource simultaneously, potentially leading to data corruption, race conditions, or inconsistent state.

Common Concurrency Solutions

Optimistic Locking

Uses version numbers to detect data changes before updates. Best for low-conflict scenarios.

Pros:

  • High performance (no blocking)
  • Good scalability

Cons:

  • Must handle version mismatch exceptions
  • Requires retry logic for failed operations

Pessimistic Locking

Locks resources before access to prevent conflicts. Best for high-conflict scenarios.

Pros:

  • Guarantees data consistency
  • No version conflicts

Cons:

  • Performance overhead
  • Potential for deadlocks
  • Reduced scalability
Deadlock

- A deadlock is a situation where two or more processes are unable to continue because they are each waiting for the other to finish, creating a cycle of dependency.
- For example, Thread A locks Resource 1 and waits for Resource 2, while Thread B locks Resource 2 and waits for Resource 1.
- Proper resource allocation, lock ordering, and timeout strategies can help prevent deadlocks.

Other Approaches

  • Synchronized/Mutex: Java/Kotlin synchronization primitives for memory-level concurrency
  • Atomic Classes: For simple atomic operations (e.g., AtomicInteger)
  • Database Transactions + Isolation Levels: Control concurrent access at the database level
  • Asynchronous Queues: Process tasks sequentially to avoid conflicts

🔧 Kotlin + Spring Data JPA Examples

1. Optimistic Locking Implementation

Entity with Version Control

@Entity
class Product(
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0,

  var name: String,
  var stock: Int,

  @Version
  var version: Long? = null
)

Repository

interface ProductRepository : JpaRepository<Product, Long>
// Uses standard findById() - JPA handles version checking automatically

Service with Retry

@Service
class ProductService(private val productRepository: ProductRepository) {
    
  @Transactional
  @Retryable(value = [ObjectOptimisticLockingFailureException::class], maxAttempts = 3)
  fun purchaseProductOptimistic(productId: Long, quantity: Int): Product {
    val product = productRepository.findById(productId)
        .orElseThrow { RuntimeException("Product not found") }

    if (product.stock < quantity) throw RuntimeException("Not enough stock")
    
    product.stock -= quantity
    return productRepository.save(product) // JPA detects version mismatch
  }
}

2. Pessimistic Locking Implementation

Repository with Locking

interface ProductRepository : JpaRepository<Product, Long> {

  @Lock(LockModeType.PESSIMISTIC_WRITE)
  @Query("SELECT p FROM Product p WHERE p.id = :id")
  fun findByIdWithLock(id: Long): Optional<Product>
}

Service

@Service
class ProductService(private val productRepository: ProductRepository) {

  @Transactional
  fun purchaseProductPessimistic(productId: Long, quantity: Int): Product {
    val product = productRepository.findByIdWithLock(productId)
        .orElseThrow { RuntimeException("Product not found") }

    if (product.stock < quantity) throw RuntimeException("Not enough stock")
    
    product.stock -= quantity
    return productRepository.save(product)
  }
}

3. Transaction Isolation Levels

// Example 1: Read-only report - need consistent snapshot
@Transactional(isolation = Isolation.REPEATABLE_READ, readOnly = true)
fun generateReport(productId: Long): Report {
  val product = productRepository.findById(productId)
  // Ensure data consistency during report generation
}

// Example 2: Real-time stock check - need latest data  
@Transactional(isolation = Isolation.READ_COMMITTED)
fun getCurrentStock(productId: Long): Int {
  return productRepository.findById(productId).stock
}

Baeldung - Transaction Isolations

When to Use Each Approach

Scenario Recommended Approach Reason
Low conflict frequency Optimistic Locking Better performance, minimal blocking
High conflict frequency Pessimistic Locking Reduces retry overhead
Simple counters Atomic Classes Lightweight, lock-free
Complex workflows Database Transactions ACID guarantees
High-throughput systems Asynchronous Queues Eliminates contention

References