Concurrency in the interview round

Part of the free Low-Level Design course on LogicWiz, in the chapter: Concurrency in LLD. The class-diagram exercises run in the app.

Concurrency in the interview round

"Your class diagram is clean. Now tell me what happens when two users book the same slot at the same moment."

Priya, playing the interviewer in a mock round


Raise it before they ask

Arjun had drawn a neat design and walked through one booking. Then Priya asked her question, and he had no answer ready. "I thought concurrency was for the senior round," he said. "I only drew classes."

That belief costs marks. Most LLD problems keep shared data: seats, slots, stock, wallets, counters. An interviewer wants to see that you know a design can work for one user and fail for two. If you raise it yourself, you also choose the moment and the words.

The best moment is after your first working flow and before the extensions. One sentence is enough: "Before I move on: if two users do this at the same moment, which data do they share?" Then use the checklist below.

You will meet this again in the problem library. The Movie Ticket Booking problem must never sell a seat twice, so it locks per show. The Rate Limiter problem keeps a bucket per key, and its token bucket is the design from the lesson on thread-safe designs. The LRU Cache and Elevator problems have thread-safety sections too.

💡 Tip: Do not talk about threads for ten minutes. Raise it, answer it in about 90 seconds, and return to the design. Interviewers can always ask for more.

The four-question checklist

Ask these questions in order. Each one can end the discussion early.

  1. What is shared? Shared data is anything that more than one thread can reach, such as a field of a service object or a map in a static field. A local variable that only one thread uses is not shared.
  2. Is it mutable? If nothing changes it after it is built, it needs no lock. A record Money(long paise) can go to a hundred threads safely. Immutable beats locked, because there is nothing to forget.
  3. What is the invariant? An invariant is a rule that must be true every time no thread is in the middle of an update. Examples: booked never exceeds capacity, tokens are never negative, the total of two wallets never changes.
  4. What is the smallest lock that protects it? One lock per slot is better than one lock for the whole service. Put only the check and the update inside, and keep slow work such as sending an SMS outside.

Apply it to slot booking. The slot map is shared. The booked count is mutable. The invariant is "booked never exceeds capacity". The smallest lock is one ReentrantLock per slot, held for the check and the increment, inside the booking service.

Protect an invariant yourself below. SwiftKart gives a coupon campaign a budget, and the rule is that the discount given must never pass it. The starter lets callers change the budget directly.

Explain it out loud in 90 seconds

Practise the same five beats every time:

  1. The shared data, about 10 seconds.
  2. The invariant, about 15 seconds.
  3. The race, as one bad ordering of two threads, about 20 seconds.
  4. The fix and why it is the smallest, about 30 seconds.
  5. The limits and the test, about 15 seconds.

Here is how it sounds for slot booking.

The shared data is the map of slots, because every request thread can reach it. The rule is that booked never goes above capacity. The danger is check-then-act: two threads both see 19 of 20 and both book. So I keep one lock per slot inside the booking service, and the lock covers only the check and the increment. The SMS goes out after the lock is released. This protects one process. With many servers I would add a version check in the database. I would test it with eight threads that start together, and check that booked never passes capacity.

Questions about locks, singletons and deadlock

"Why not make every method synchronized?" There are three reasons. First, two safe calls in a row are not a safe pair. The code below uses a Collections.synchronizedList, where every call is locked, and still lets the same coupon in twice.

void applyOnce(String code) {              // each call is safe, the pair is not
    if (!applied.contains(code)) {
        applied.add(code);
    }
}
void applyOnceSafely(String code) {
    synchronized (applied) {               // one lock around check and add
        if (!applied.contains(code)) {
            applied.add(code);
        }
    }
}

Two threads applied 500 different codes each. In three runs of 2,000 rounds, the first version ended with the wrong size in 216, 217 and 183 rounds. The second version was right every time. Second, one lock for everything makes unrelated work wait in a line. Third, methods that call other objects while holding the lock invite deadlock.

"Show me a singleton that is safe for threads, using double-checked locking. Why volatile?"

private static volatile PricingConfig instance;

static PricingConfig get() {
    PricingConfig local = instance;              // first check, no lock
    if (local == null) {
        synchronized (PricingConfig.class) {
            local = instance;                    // second check, with the lock
            if (local == null) {
                instance = local = new PricingConfig();
            }
        }
    }
    return local;
}

Creating an object has three steps: get memory, run the constructor, store the reference. Without volatile, Java may reorder them, so another thread could see a non-null reference to an object whose constructor has not finished. volatile forbids that. Eight threads called get() and created one instance. The failure without volatile was not provoked here. The language rules allow it, and a test that passes would prove nothing. In an interview, say that the holder class or an enum is simpler, as the singleton lesson showed.

"What are the four conditions for deadlock?" All four must hold at once. Remove one, and deadlock cannot happen.

Condition Meaning Break it by
Mutual exclusion Only one thread can hold the lock Sharing nothing, or immutable data
Hold and wait A thread holds one lock and waits for another Taking every lock at once, or tryLock
No preemption A lock cannot be taken from its holder tryLock with a timeout, then release
Circular wait A waits for B while B waits for A Locking in one fixed order

Circular wait is the one you break most often, by lock ordering.

"What are livelock and starvation?" In a livelock, threads keep reacting to each other and keep changing state, but none makes progress, like two people who step aside in the same direction in a corridor. In starvation, one thread never gets its turn because others always go first, and a fair lock such as new ReentrantLock(true) favours the thread that has waited longest.

Questions about collections and tests

"Should I use a thread-safe collection or synchronize myself?" A thread-safe collection protects each single call. It does not protect a series of calls. Use ConcurrentHashMap with merge, compute or putIfAbsent when one key's update fits in one call. Use ConcurrentHashMap.newKeySet(), whose add tells you atomically whether the item was new. Use CopyOnWriteArrayList for a list that is read often and changed rarely, such as a list of listeners. When a rule spans several calls or several collections, use one lock of your own, and iterate a synchronizedList only while holding its lock.

"What does Thread.sleep in a concurrency test hide?" It hides two things. It guesses how long work takes, so it fails on a slow machine and wastes time on a fast one. Worse, it can keep threads apart so they never overlap, and the race never has a chance. The first test below slept 20 milliseconds between starting threads. The second released all threads together.

// Sleep version: the threads never overlap
for (int t = 0; t < 8; t++) {
    threads[t] = new Thread(() -> bookMany(slot));
    threads[t].start();
    Thread.sleep(20);
}
// Barrier version: all 8 threads are released at the same moment
CyclicBarrier go = new CyclicBarrier(8);
for (int t = 0; t < 8; t++) {
    futures.add(pool.submit(() -> { go.await(); bookMany(slot); return null; }));
}

The test booked a slot of 1,000 places with 8,000 attempts and checked that exactly 1,000 succeeded. Against a booking method with a race, the sleep test passed in 100 of 100 runs, in all three attempts. The barrier test failed in 20, 25 and 37 of 100 runs. So start threads together, wait with Future.get(timeout) or CountDownLatch.await(timeout) instead of sleeping, and check the invariant, not the order. A passing test is still only evidence. Say that out loud.

Mistakes that lose marks

  • Saying nothing about concurrency until the interviewer asks.
  • Marking every method synchronized and calling the design safe.
  • Making callers take the lock, so one forgotten call breaks the rule.
  • Using a plain HashMap across threads, or containsKey then put on a concurrent map.
  • Sending an SMS or calling a payment service while holding a lock.
  • Taking a lock without releasing it in a finally block.
  • Saying volatile makes a counter safe. It gives visibility, not atomicity.
  • Forgetting to say that a Java lock protects one process, not twelve servers.
  • Claiming that one passing test proves the design is safe.

📌 Summary: Raise the two-users question yourself. Ask what is shared, what is mutable, what the invariant is, and which lock is smallest. Prefer immutable data. Explain in 90 seconds, and test with threads that start together.

What Arjun learns next

Arjun now has the tools and the words for concurrency. The next chapter puts the whole course together in worked case studies. He builds complete designs step by step, and each one will ask him to say, out loud, what is shared and what protects it.

Check your understanding

  1. In a mock round you have finished the class diagram of a seat-booking design. What is the strongest next move on concurrency?
  2. A candidate says: "Every method of my coupon list is synchronized, so `if (!contains(c)) add(c)` is safe." What is wrong?
  3. In double-checked locking for a singleton, why must the instance field be `volatile`?
  4. Which change breaks one of the four conditions for deadlock in the wallet transfer, and which condition is it?
  5. A test starts 8 booking threads, with a 20 ms sleep between starts, and always passes. Why is that weak evidence of thread safety?