DEV Community

Cover image for Handling Concurrent Requests and Duplicate Checkouts in Spring Boot
Shitanshu Jha
Shitanshu Jha

Posted on

Handling Concurrent Requests and Duplicate Checkouts in Spring Boot

While building ShopEase, my e-commerce backend project, I reached a stage where implementing CRUD endpoints was no longer enough.

I needed to think about what happens when multiple customers try to update the same inventory or submit the same checkout request simultaneously.

That led me to work on a new backend milestone focused on high-concurrency handling and idempotency protection.

Here are the key implementations and what I learned from them.

  1. Atomic SQL Updates for Inventory

Consider a product with five units in stock. Multiple purchase requests may arrive at almost the same time.

Reading the stock in Java, subtracting a quantity, and saving the entity can introduce race conditions if competing requests read the same initial value.

I added a conditional atomic SQL update:

UPDATE products
SET stock = stock - :qty
WHERE id = :id
AND stock >= :qty;

The database performs the stock check and deduction as one statement.

The affected-row count indicates whether the update succeeded, allowing the application to handle insufficient stock.

This approach reduces unnecessary application-side read-modify-write operations. It does not mean the database performs the operation without any internal locking.

  1. Pessimistic vs. Optimistic Locking

I worked with both locking strategies in DatabaseConcurrencyService.

Pessimistic locking

Pessimistic locking requests a write lock on a database record while the transaction performs its work.

With JPA, this can be implemented using LockModeType.PESSIMISTIC_WRITE.

It is useful when conflicting updates are likely, although excessive lock contention can reduce throughput.

Optimistic locking

Optimistic locking detects conflicting changes rather than preventing every competing transaction from proceeding.

I added a JPA version field:

@version
private Long version;

Hibernate uses the version to detect stale updates. A conflicting update can fail and require the application to handle or retry the operation.

My takeaway: Both strategies are useful, but they address concurrency differently. The choice depends on the workload and conflict rate.

  1. Spring Transaction Proxies and Executor Threads

One important detail I worked on was self-proxy injection.

Spring typically applies @Transactional behavior through a proxy. A direct call such as this.someTransactionalMethod() can bypass the proxy and therefore bypass the expected transaction interception.

Calling the method through the injected Spring proxy allows the transaction interceptor to run.

There is another important detail: an executor thread does not automatically inherit the caller's transaction. A transactional method invoked through the proxy needs to establish its own transaction as required.

This was a useful reminder that asynchronous execution and transaction boundaries must be considered together.

  1. Idempotency Protection for Checkout

Checkout requests can be repeated because of network issues, client retries, or accidental double-clicks.

I implemented IdempotencyKey validation to detect duplicate checkout requests and return 409 Conflict for duplicates.

The basic flow is:

Receive a checkout request with an idempotency key.

Validate whether the key has already been used.

Process a new request.

Reject a duplicate request.

For reliable production behavior, checking and recording the key must be atomic. A database uniqueness constraint is one common way to enforce this. The design also needs clear rules for retries, failed requests, and payment side effects.

The goal is to avoid processing the same logical checkout operation more than once.

  1. JPA Versioning and Application Configuration

I added the version column required for optimistic locking and resolved a Spring bean collision.

These changes help detect stale database updates and allow the application context to initialize with the intended bean configuration.

Key Takeaways

This milestone helped me understand that reliable backend development involves more than writing endpoints.

Use atomic database operations for conditional inventory updates.

Choose pessimistic or optimistic locking according to the workload.

Understand how Spring proxies affect transaction management.

Design executor-thread work with explicit transaction boundaries.

Protect checkout side effects against duplicate requests.

Use JPA versioning to detect conflicting updates.

Final Thoughts

Working on ShopEase is helping me connect Java, Spring Boot, database concurrency, and real e-commerce requirements.

This milestone is a step toward making inventory management and checkout processing more reliable under concurrent load.

I'm continuing to build and improve ShopEase one backend milestone at a time.

Tech stack: Java, Spring Boot, Spring Data JPA, Hibernate, MySQL

Java #SpringBoot #BackendDevelopment #MySQL

Top comments (0)