☕ A Walk Down Memory Lane: My First Bug Nightmare
About 9 years ago, as a fresher, I worked on a billing system for a retail application. The system was accessed both from mobile and web UI, and the actual billing logic was written in stored procedures (SQL-based backend).
Everything looked fine... until customers started reporting weird issues:
๐ The Bug: Bills Were Swapping Items! ๐ฑ
๐งพ Scenario:
-
Two different users generated separate bills around the same time
-
One from Mobile, the other from Web
-
The output bills had mixed-up items — each invoice had products that belonged to the other one! ๐งจ
๐ Initial Questions:
"Is this a DB issue?"
"Are stored procedures broken?"
"Race condition in app layer?"
Turns out...
๐ซ The code calling stored procedures wasn’t thread-safe.
There was no synchronized
mechanism to ensure each bill transaction was isolated.
๐ง The Root Cause Analysis (RCA):
We were calling a shared
BillingService.calculate()
method across different threads, and it wasn’t synchronized. When multiple threads hit the same method at the same time, data leakage occurred between bills.
๐ The stored procedure was NOT the problem — the problem was how the Java app was managing calls to it.
๐คฏ A Doubt I Had Later...
๐ญ "In modern microservices — or even back then — each request runs in its own thread. So why would two requests ever clash? Shouldn’t each request be isolated in memory?"
✅ You're right — each incoming HTTP request is handled by a separate thread. But...
⚠️ Here's What Was Actually Happening:
The Java service class (BillingService
) had some shared, mutable fields — like:
public class BillingService {
private List<Item> items = new ArrayList<>(); // shared state ❌
public void calculate(BillRequest req) {
items.clear(); // modifies shared state
items.addAll(req.getItems());
billingDao.callStoredProcedure(items);
}
}
So multiple threads using this shared object were modifying the same list at the same time — causing one bill's data to show up in another. ๐คฏ
๐ ️ How We Fixed It: With synchronized
๐ก
We locked access to the calculate()
method so only one thread could execute it at a time:
public class BillingService {
public synchronized void calculate(BillRequest req) {
List<Item> items = new ArrayList<>(); // local state ✅
items.addAll(req.getItems());
billingDao.callStoredProcedure(items);
}
}
OR
public class BillingService {
public void calculate(BillRequest req) {
synchronized (this) {
List<Item> items = new ArrayList<>();
items.addAll(req.getItems());
billingDao.callStoredProcedure(items);
}
}
}
✅ No more shared state.
✅ No more item swapping.
✅ Happy customers.
๐ What is synchronized
in Java?
synchronized
is a Java keyword used to prevent concurrent thread access to a block or method.
It ensures:
-
Only one thread can execute the synchronized code at a time
-
Prevents race conditions and data corruption
๐งฑ Two Ways to Use synchronized
:
1️⃣ Synchronized Method:
public synchronized void processBill(BillRequest req) {
// entire method is locked
}
2️⃣ Synchronized Block:
public void processBill(BillRequest req) {
synchronized(this) {
// only this part is locked
}
}
๐ Block-level is better for performance — it only locks what needs locking.
๐ Bonus: Should I Use Locks Instead?
Yes, in high-concurrency apps, consider:
-
ReentrantLock
-
ReadWriteLock
-
Optimistic locking in DB (version columns)
But for simple, state-sensitive flows — synchronized works beautifully ๐ฏ
๐ Wrapping Up
This bug taught me a valuable lesson early in my career:
Always think about thread safety — especially when dealing with shared resources like billing engines or inventory systems.
๐ Synchronization might seem simple, but when forgotten, it can lead to serious production messes.
Got a wild thread-safety story or billing bug nightmare? Drop it in the comments — I’d love to hear it.
Until next time,
– Anand ☕ @ Java Bean Bag
Comments
Post a Comment