Demonstrating Semaphores Application in Spring Boot - Java

A practical guide to solving the Bounded-Buffer and Readers-Writers problems in Java.
Introduction
In modern backend development, we often rely on "magic" annotations like @Transactional or synchronized blocks without looking under the hood. But when you are building high-stakes financial APIs or inventory systems for flash sales, understanding the fundamentals of thread synchronization is what separates code that "works" from code that scales.
I recently completed a deep dive into the theoretical roots of synchronization, analyzing classical solutions like Dekker’s and Peterson’s Algorithms. While these algorithms were groundbreaking for understanding mutual exclusion using only shared memory, they suffer from limitations like busy-waiting and complexity in modern architectures.
Today, we use Semaphores.
For a quick recap from my last ariticle here, semaphore is a robust synchronization primitive that consists of an integer value and two atomic operations: wait() (decrement) and signal() (increment). In this guide, I will show you how to take the theoretical models of the Bounded-Buffer and Readers-Writers problems and implement them in a real-world Spring Boot application.
1. The Bounded-Buffer Problem (The Order Queue)
The Theory
Also known as the Producer-Consumer problem, this involves two processes sharing a fixed-size buffer. The challenges are clear:
Overflow: Producers must not add data to a full buffer.
Underflow: Consumers must not remove data from an empty buffer.
Mutual Exclusion: Only one process can modify the buffer at a time.
The Real-World Scenario
Imagine an E-commerce "Order Processing" service. During a flash sale, thousands of users (Producers) place orders. If the warehouse system (Consumer) is slow, we need to block incoming orders once our internal queue reaches capacity to prevent a crash.
The Implementation
We will use three semaphores, mapped directly from the academic solution:
mutex(Binary Semaphore): Ensures exclusive access to the list.empty(Counting Semaphore): Tracks available slots.full(Counting Semaphore): Tracks items ready to be processed.
import org.springframework.stereotype.Service;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Semaphore;
@Service
public class OrderQueueService {
private final Queue<String> orderBuffer = new LinkedList<>();
private final int CAPACITY = 5; // Buffer Size (N)
// Semaphores mapped from theory
private final Semaphore mutex = new Semaphore(1); // Mutual Exclusion
private final Semaphore empty = new Semaphore(CAPACITY); // Counts empty slots
private final Semaphore full = new Semaphore(0); // Counts filled slots
// PRODUCER: The User placing an order
public void placeOrder(String orderId) throws InterruptedException {
System.out.println("Attempting to place order: " + orderId);
// 1. Wait for an empty slot (prevents overflow)
empty.acquire();
// 2. Enter Critical Section
mutex.acquire();
try {
orderBuffer.add(orderId);
System.out.println("Order added: " + orderId + ". Buffer size: " + orderBuffer.size());
} finally {
// 3. Leave Critical Section & Signal that buffer has data
mutex.release();
full.release();
}
}
// CONSUMER: The Warehouse processing the order
public void processOrder() throws InterruptedException {
// 1. Wait for a filled slot (prevents underflow)
full.acquire();
// 2. Enter Critical Section
mutex.acquire();
try {
String order = orderBuffer.poll();
System.out.println("Processing order: " + order);
Thread.sleep(1000); // Simulate heavy processing
} finally {
// 3. Leave Critical Section & Signal that a slot is free
mutex.release();
empty.release();
}
}
}
2. The Readers-Writers Problem (Dynamic Pricing)
The Theory
This problem involves a shared resource accessed by two types of processes: Readers (who only read) and Writers (who modify) .
Multiple readers can read simultaneously.
Only one writer can write at a time.
Crucially: Readers and writers cannot access the data simultaneously.
The Real-World Scenario
Consider a product price on a high-traffic page. Thousands of users (Readers) view the price every second. Occasionally, an admin (Writer) updates the price. We want to allow high throughput for readers but ensure that when a write happens, no one sees "half-updated" data.
The Implementation
We will implement the "Readers Priority" solution. The first reader to arrive locks the door for writers. Subsequent readers can enter freely. The last reader to leave unlocks the door .
Java
import org.springframework.stereotype.Service;
import java.util.concurrent.Semaphore;
@Service
public class PriceService {
private double productPrice = 999.99;
private int readCount = 0; // Tracks active readers [cite: 166]
// Semaphores
private final Semaphore mutex = new Semaphore(1); // Protects readCount
private final Semaphore writeLock = new Semaphore(1); // Controls write access [cite: 166]
// READER: User viewing price
public double viewPrice() throws InterruptedException {
// Entry Section
mutex.acquire();
readCount++;
if (readCount == 1) {
// If I am the first reader, I lock out the writers [cite: 171]
writeLock.acquire();
}
mutex.release();
// CRITICAL SECTION (Reading)
System.out.println("Reader inside. Active readers: " + readCount);
double price = this.productPrice;
Thread.sleep(200); // Simulate read latency
// Exit Section
mutex.acquire();
readCount--;
if (readCount == 0) {
// If I am the last reader, I unlock the writers [cite: 179]
writeLock.release();
}
mutex.release();
return price;
}
// WRITER: Admin updating price
public void updatePrice(double newPrice) throws InterruptedException {
// Writers require exclusive access [cite: 184]
writeLock.acquire();
try {
System.out.println("WRITER updating price to: " + newPrice);
this.productPrice = newPrice;
Thread.sleep(1000); // Simulate write latency
} finally {
writeLock.release(); // [cite: 186]
}
}
}
3. Bringing It Together (The API Layer)
To test this, we hook these services up to a simple Spring Boot Controller.
Java
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ConcurrencyController {
private final OrderQueueService orderService;
private final PriceService priceService;
@PostMapping("/order/{id}")
public String placeOrder(@PathVariable String id) throws InterruptedException {
orderService.placeOrder(id);
return "Order Placed";
}
@GetMapping("/price")
public double getPrice() throws InterruptedException {
return priceService.viewPrice();
}
}
How to Verify It Works
Bounded Buffer: Set your
CAPACITYto 2. Send 3 requests to/api/order/x. You will see the third request "hang" indefinitely in your HTTP client. It is waiting onempty.acquire()—exactly as the algorithm dictates!Readers-Writers: Fire off 10 concurrent GET requests to
/api/priceand 1 PUT request to update it. You will see the GET requests complete in parallel, while the PUT request waits for the active count to drop to zero.
Conclusion
We have covered abstract concepts of Critical Sections and Mutual Exclusion to working Java code. While modern frameworks offer higher-level abstractions, understanding Semaphores gives you the power to debug complex race conditions and architect systems that handle pressure without crashing.
Next Up: In the next part of this series, I’ll be switching gears to the Node.js ecosystem to see how we handle these same problems in a single-threaded Event Loop using NestJS .
If you found this useful, connect with me on LinkedIn where I share more about building scalable backend systems.



