Skip to content
Back EndJava

Building a High-Concurrency Mobile Flash Sale Backend with Spring Boot 4 and Redis

8 min readsalahtobok
High Concurrency Mobile Flash Sale Spring Boot Redis

Flash sales on mobile apps are extremely popular. However, they bring a big technical challenge. When thousands of users tap the “Buy” button at the exact same second, your backend can easily crash or oversell inventory. If you only have 100 phones, you cannot sell 101. Standard databases like PostgreSQL struggle to lock rows fast enough under such heavy concurrent load.

In this step-by-step tutorial, you will learn how to build a high-concurrency mobile flash sale backend. We will use Spring Boot 4, Java 25, and Redis. You will see how to use a Redis Lua script to instantly lock and decrement inventory. Then, we will process the actual order asynchronously in PostgreSQL.

What You’ll Build

You will build a Spring Boot 4 REST API that handles purchase requests. The system will use Redis to track inventory in memory. A Lua script will guarantee that no two users can buy the same item at the same time. After Redis confirms the stock, the system will save the final order into PostgreSQL.

Architecture Diagram Spring Boot Redis
The system architecture: Mobile Apps -> Spring Boot API -> Redis -> PostgreSQL.

Prerequisites

  • Java 25 (or newer) installed on your machine.
  • Maven 3.9+ for building the project.
  • Docker running (for Testcontainers and our local database setup).
  • Basic knowledge of Spring Boot and REST APIs.

Step 1: Create the Spring Boot 4 Project

First, we need to set up our Maven project. We will include dependencies for Spring Web, Spring Data Redis, Spring Data JPA, and Testcontainers. Testcontainers will automatically start Redis and PostgreSQL when we run our tests.

Create a file named pom.xml in your new project folder:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.0</version>
    </parent>
    <groupId>com.webcodein.architecture</groupId>
    <artifactId>high-concurrency-mobile-flash-sale-spring-boot-redis</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <java.version>25</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers-junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers-postgresql</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-testcontainers</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Code walkthrough:

  • We declare spring-boot-starter-web to create REST endpoints.
  • We declare spring-boot-starter-data-redis to connect to our Redis instance.
  • We add Testcontainers dependencies so we can write integration tests without installing databases locally.

Step 2: Create the Application Entry Point

Every Spring Boot application needs a main class. We will also enable asynchronous processing because we want to save orders in the background.

Create src/main/java/com/webcodein/architecture/flashsale/FlashSaleApplication.java:

package com.webcodein.architecture.flashsale;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class FlashSaleApplication {
    public static void main(String[] args) {
        SpringApplication.run(FlashSaleApplication.class, args);
    }
}

The @EnableAsync annotation allows us to run methods in separate background threads later.

Docker Stack Spring Boot Postgres Redis
Our final stack will run seamlessly in Docker containers.

Step 3: Define the Order Domain

When a user successfully buys an item, we need to record the order in our PostgreSQL database. Let’s create a simple JPA Entity and a Repository to manage it.

Create src/main/java/com/webcodein/architecture/flashsale/domain/Order.java:

package com.webcodein.architecture.flashsale.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productId;
    private String userId;

    public Order() {}

    public Order(String productId, String userId) {
        this.productId = productId;
        this.userId = userId;
    }

    public Long getId() { return id; }
    public String getProductId() { return productId; }
    public String getUserId() { return userId; }
}

Next, create the repository interface at src/main/java/com/webcodein/architecture/flashsale/domain/OrderRepository.java:

package com.webcodein.architecture.flashsale.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
}

We also need to tell Hibernate to automatically create our database tables. Add this to src/main/resources/application.yml:

spring:
  jpa:
    hibernate:
      ddl-auto: update

Step 4: Create the Redis Lua Script

This is the most critical part of the flash sale. A standard “read, check, update” process in Java will fail under high concurrency. If two threads read the inventory as “1” at the same time, they will both subtract 1, and the inventory will go below zero (overselling).

Redis evaluates Lua scripts atomically. This means no other command can run while the script is executing.

Redis Lua Script Race Condition Lock
The Lua script blocks race conditions by locking the transaction in Redis.

Create src/main/resources/scripts/decrement_inventory.lua:

local inventory_key = KEYS[1]
local requested_quantity = tonumber(ARGV[1])

local current_inventory = tonumber(redis.call('get', inventory_key) or "0")

if current_inventory >= requested_quantity then
    redis.call('decrby', inventory_key, requested_quantity)
    return 1 -- Success
else
    return 0 -- Failed, out of stock
end

Now, let’s configure Spring Boot to load this script. Create src/main/java/com/webcodein/architecture/flashsale/config/RedisConfig.java:

package com.webcodein.architecture.flashsale.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

@Configuration
public class RedisConfig {
    @Bean
    public RedisScript<Long> decrementInventoryScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("scripts/decrement_inventory.lua"));
        script.setResultType(Long.class);
        return script;
    }
}

Step 5: Process the Order Asynchronously

After Redis confirms the purchase, we should not make the user wait while we write to PostgreSQL. Writing to a relational database is slow compared to Redis.

Redis vs Postgres IO Comparison
Redis handles the fast I/O checks, while Postgres handles the durable async writing.

Create an async service at src/main/java/com/webcodein/architecture/flashsale/service/OrderProcessor.java:

package com.webcodein.architecture.flashsale.service;

import com.webcodein.architecture.flashsale.domain.Order;
import com.webcodein.architecture.flashsale.domain.OrderRepository;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class OrderProcessor {
    private final OrderRepository orderRepository;

    public OrderProcessor(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Async
    public void processOrder(String productId, String userId) {
        // We simulate a small processing delay
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        orderRepository.save(new Order(productId, userId));
    }
}

Step 6: Build the Flash Sale Service

Now we connect Redis and the order processor. We execute the Lua script, and if it returns `1`, we know the inventory was successfully decremented.

Create src/main/java/com/webcodein/architecture/flashsale/service/FlashSaleService.java:

package com.webcodein.architecture.flashsale.service;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class FlashSaleService {
    private final StringRedisTemplate redisTemplate;
    private final RedisScript<Long> decrementInventoryScript;
    private final OrderProcessor orderProcessor;

    public FlashSaleService(StringRedisTemplate redisTemplate, RedisScript<Long> decrementInventoryScript, OrderProcessor orderProcessor) {
        this.redisTemplate = redisTemplate;
        this.decrementInventoryScript = decrementInventoryScript;
        this.orderProcessor = orderProcessor;
    }

    public boolean purchase(String productId, String userId) {
        String inventoryKey = "inventory:" + productId;
        
        Long result = redisTemplate.execute(
            decrementInventoryScript,
            List.of(inventoryKey),
            "1"
        );

        if (result != null && result == 1L) {
            orderProcessor.processOrder(productId, userId);
            return true;
        }
        
        return false;
    }
}
Sequence Flow Flash Sale
The sequence flow mapping exactly how the request travels.

Step 7: Expose the REST Endpoint

Finally, we need a controller so mobile apps can send purchase requests.

Create src/main/java/com/webcodein/architecture/flashsale/controller/FlashSaleController.java:

package com.webcodein.architecture.flashsale.controller;

import com.webcodein.architecture.flashsale.service.FlashSaleService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/flash-sale")
public class FlashSaleController {
    private final FlashSaleService flashSaleService;

    public FlashSaleController(FlashSaleService flashSaleService) {
        this.flashSaleService = flashSaleService;
    }

    @PostMapping("/{productId}/purchase")
    public ResponseEntity<String> purchase(@PathVariable String productId, @RequestParam String userId) {
        boolean success = flashSaleService.purchase(productId, userId);
        if (success) {
            return ResponseEntity.ok("Purchase successful! Order is being processed.");
        } else {
            return ResponseEntity.badRequest().body("Sold out!");
        }
    }
}

Testing the Application

To prove our system prevents overselling, we will write an integration test. We will simulate 100 concurrent users trying to buy an item that only has a stock of 10.

Check the companion repository linked below to view the full FlashSaleIntegrationTest.java file. Run the test with Maven:

mvn clean verify

You will see that exactly 10 requests succeed, and 90 requests fail. No overselling occurs, and 10 orders are asynchronously saved to the database.

Running with Docker

The companion code includes a Dockerfile and a docker-compose.yml file. You can start the entire stack (Postgres, Redis, and Spring Boot) with one command:

docker compose up --build

Then, simulate a purchase in your terminal:

curl -X POST "http://localhost:8080/api/flash-sale/IPHONE15/purchase?userId=user123"

What You Learned

  • How to execute atomic Lua scripts in Redis to prevent data race conditions.
  • How to separate fast in-memory checks from slower database writes.
  • How to process background tasks asynchronously in Spring Boot.
  • How to orchestrate complex services using Testcontainers for reliable integration testing.

Next Steps

Explore related high-throughput and distributed systems tutorials on WebCodein:

Companion Code

The complete, runnable Spring Boot 4 project for this tutorial, including automated high-concurrency integration tests with Testcontainers, Redis, and PostgreSQL, is available on our public GitHub repository:
Workshop Companion Code: Building a High-Concurrency Mobile Flash Sale Backend with Spring Boot 4 and Redis

References

Comments