---
title: 'Concurrency in Low-Level Design Interviews: Correctness, Coordination, and Scarcity'
source: 'https://youtube.com/watch?v=d8rmosXttTE'
video_id: 'd8rmosXttTE'
date: 2026-08-04
duration_sec: 1350
---

# Concurrency in Low-Level Design Interviews: Correctness, Coordination, and Scarcity

> Source: [Concurrency in Low-Level Design Interviews: Correctness, Coordination, and Scarcity](https://youtube.com/watch?v=d8rmosXttTE)

## Summary

This video provides a comprehensive guide to tackling concurrency issues in low-level design interviews. The presenter, Evan, categorizes concurrency problems into three main types: correctness, coordination, and scarcity. He explains each category with practical examples, demonstrates the appropriate tools and primitives (locks, atomic variables, blocking queues, semaphores), and offers a mental checklist for solving these problems in interviews.

### Key Points

- **Three Categories of Concurrency Issues** [00:28] — Concurrency issues in low-level design interviews fall into three categories: correctness, coordination, and scarcity. Understanding these categories helps in recognizing and solving concurrency challenges.
- **Concurrency in Interview Problems** [00:52] — Four out of seven (soon to be eight) common interview questions on HelloInterview.com have concurrent aspects, indicating the importance of concurrency in low-level design interviews.
- **Ticket Booking Example** [01:17] — A ticket booking service (similar to Ticketmaster) is used as an example. In a multi-threaded environment, two users booking the same seat simultaneously can lead to a race condition, illustrating a correctness issue.
- **Correctness Issues: Check-Then-Act** [03:06] — The check-then-act pattern is a common correctness issue where a thread checks a condition and then acts, but another thread can intervene in between. The fix is to make the check and action atomic, typically using a lock.
- **Correctness Issues: Read-Modify-Write** [06:38] — Read-modify-write is another correctness pattern, such as incrementing a counter. It involves reading a value, modifying it, and writing it back. The fix is to use atomic variables (e.g., AtomicInteger in Java) or locks.
- **Coordination Issues: Producer-Consumer** [09:58] — Coordination issues arise when work is passed between threads, e.g., a producer putting tasks into a queue and consumers taking them. The naive spin-wait or sleep-and-poll approaches waste CPU or introduce latency. The solution is a blocking queue.
- **Coordination Issues: Backpressure** [14:16] — When work arrives faster than consumers can handle, an unbounded queue can cause memory issues. The solution is a bounded blocking queue, which applies backpressure by blocking producers when the queue is full.
- **Scarcity Issues: Semaphores** [16:08] — Scarcity issues occur when limiting concurrent access to finite resources, e.g., an external API with a rate limit. Semaphores are used to control the number of concurrent accesses. Ensure permits are released even on exceptions using try-finally.
- **Scarcity Issues: Connection Pools** [18:34] — Managing actual objects with state, like database connections, can be done using a blocking queue as a pool. Threads take a connection, use it, and put it back, ensuring efficient reuse.
- **Mental Checklist for Interviews** [20:58] — In a low-level design interview, ask: Is there shared state? (correctness -> lock/atomic). Is work flowing between threads? (coordination -> blocking queue). Is there a fixed limit? (scarcity -> semaphore/pool).

### Conclusion

Concurrency in low-level design interviews can be systematically approached by categorizing issues into correctness, coordination, and scarcity. Recognizing these patterns and applying the appropriate primitives (locks, atomics, blocking queues, semaphores) will help you solve most concurrency problems effectively.

## Transcript

Hey everybody, welcome back to the channel. For those of you who are new here, I'm Evan. I'm a former Metastaff engineer and I'm the current co-founder of HelloInterview.com. If you are preparing for an upcoming software engineering interview, then you'll want to come over to Hello Interview, what you see here.
We have everything that you need to prepare for your upcoming interviews across all of the major interview types. Today's video, we're going to be focusing on low-level design. And specifically, we're going to be focused on what is oftentimes the most tricky part of low-level design interviews, which is concurrency.
And so I'm going to break down the three categories of concurrency issues that you'll see in your interviews, from correctness, coordination, and scarcity. Show you examples, the tools you need, the primitives, in order to solve each of these.
I'm going to remain pretty high level here so we can get through this quickly, but know that as you see here on the site, we have detailed breakdowns for each of them, so you can come over here and read that. We also have problem breakdowns, where we go into common interview questions.
I think four of the current seven, soon to be eight, have concurrent aspects to them. So we probe concurrency in each of them respectively. So you can read through those and see how they end up being applied.
As always, if you like what we're doing here, please don't forget to like and subscribe. Enough with intros. Let's get into it. So I want you to start by imagining that you're booking a ticket booking service similar to Ticketmaster,
but it's in process. This is not a distributed system. This is not system design. This is low-level design, which means that we're working within a single process. And so in this booking system, you can, of course, browse seats and book them.
And so you start off by writing this book seat function, which is part of your booking service class, where given a seat, you check if that seat is available, and if yes, you book it.
So I have the implementation for you here in Java, as well as in Python, if that's better for you. Now, this code may look fine to you. And to be frank, actually it is fine. In single threaded languages like TypeScript or something, then there is nothing wrong
with this. But if you're operating in a multi-threaded environment, then what's going to happen if two users try to book the same seat at exactly the same time? This would mean that two different threads are accessing the same shared state, basically
the seats map, at roughly the same time. And so let's walk through what would happen here. What you would have is you would have thread A, we can call her Alice in this case, is going to check if seat 7A is available. It is. Then thread B is also going to check if
seat 7A is available. It also is, because thread A, or Alice here, still hasn't completed her booking. And so the next thing that's going to happen is that Alex is going to book, and at the same time, then Bob is going to book. Bob's book is going to overwrite Alice's
book, and now Bob is the person with the seat. Alice thinks that she has a seat, but she shows up to the concert and sees Bob there and we're in a bunch of trouble. So this example illustrates what is the most common type of concurrency bug that shows up in interviews.
It's called correctness issues. And it's easy to miss if you're not specifically looking for it. But there are plenty of other concurrency challenges that arise in these multi-threaded systems. But they fall neatly into three main categories. First we have correctness,
like we just saw here. Second is coordination. And then third is scarcity. And once you know these three categories, you understand what they are, how they show up, and how to solve them, then you're well equipped to handle just about any concurrency challenge that's going to be
throwing your way in a low-level design and machine coding interview. Let's go through each of these categories one by one, starting with correctness. So we just saw correctness issues happen when shared state gets corrupted because two threads are trying to access
it at the exact same time. Now, the pattern that you want to recognize here that you want to keep that I opt for in your interviews is oftentimes called check then act. Check then act. Basically,
you check a condition, like right here, and then you ask based on that condition. But between the check and the ask, another thread can come in and change things. So obviously, in our ticket booking example, we checked if the seat was available and then we booked it. And that's two separate steps.
So in that gap between the check and the action, another thread can sneak in there and book that feed out from under us. And this gap is oftentimes where the bug lives. So this is the trouble area right here. And this check then ask pattern is probably the most popular concurrency issue that
ends up showing up in low-level design interviews. If you have a parking lot system with multiple entrances, then you check if the spot is empty and then assign a car to it. Check then act. Rate limiter, you check if the user is under their rate limit and then you allow the request.
In the inventory system, you check if the stock is available and then process the order. It's all the same shape and the goal is to be able to recognize that. Now maybe importantly, most importantly, the fifth, at least at a high level, is that the
check and the action need to happen together as one atomic operation. This prevents any thread from being allowed to come in between here and interrupt things. The standard tool for this is called a lock, and each programming language has its own
lock primitives. You have mutexes in things like Rust and Go, you have a synchronized lock in Java, which we'll see here in just a moment, a lock in Python, mutex or semaphores in C++. All of these allow just one thread to execute a section of code at a time, while other threads
wait on that lock to re-release. So you acquire the lock first, do your check, do your action, and then release the lock. So let's see what that would look like. In the Java case, it's going to look like this.
Java uses what's called a synchronized lock. And so everything within the synchronized lock is while the lock is held Basically if you have the synchronized block this is when we take the lock we operate and then we release the lock at the end of that synchronized block
In the case of Python, you do with lock, and that lock is typically an instance variable. And so, similar idea here, right? You first grab the lock, and then everything that is within that block has that lock held.
ends up if the lock is held, no two threads can intervene with each other. If we come back over to our diagram, it would now look like this. You'd have thread one or Alice check if things are available. Yes.
We would then book it from Alice. The status equals booked. And that whole time, Bob can't come in because Alice first grabbed the lock. And so Alice is holding the lock. She's able to do both the check and the act.
Bob is just waiting. And it's only once that lock is released and she has set the status of that seat to booked that Bob then can check is available, and now he's going to correctly be updated to understand that the answer is no.
The second most common correctness pattern that shows up is what's called read-modify-write. And so this is when you read a value, you compute something from it, and then you write it back.
So the classic example is just incrementing a counter. You have a counter here where it's currently at 1. We do count plus plus. And now this might look to you like one operation. It's one line of code, but it's actually three.
We first read the value to see what it currently is. We add one, and then we write back that new value. And so just like the example above, if two threads were doing this at the exact same time, both might read, for example, five.
Both might read, for example, five. They might both compute six. They might both then write six, and we're left with six as the final count when it should have been seven. We lost an increment. It shows up obviously in hit counters, bank account balances, inventory quantities, metric
aggregation. Basically, any time you're updating a value based on its current state, you're going to run into this read, modify, write pattern. Now, the fifth hole is the exact same idea. We need to make this whole operation, this read, modify, and write atomic.
And so for simple counters, you can actually use what's called an atomic variable, which handles this at the hardware level, which is pretty cool. And so modern CPUs, they have special instructions, like compare and swap, that do a read-modify
right in a single CPU cycle. Basically, the hardware guarantees that no other threads can see that value mid-update. So let's look at the implementation of the solution. Just like with locks, each major language provides its own primitive for an atomic integer.
And so in Java, it's called exactly that, an atomic integer. You can instantiate your atomic integer to whatever you want. In this case, 0. Maybe we'd say 10. And then anytime you want to increment it by one, instead of doing count++, you have to do count.increment in git.
And this is going to be that hardware-level atomic operation. And so this would then return back to you 11 in our case. And it's going to be using that comparing swap at the actual CPU. Now, unfortunately, Python doesn't actually support built-in atomics.
So the way to solve this in Python, unfortunately, there is no atomic that you can use. you're going to have to just do what we did above, and you're going to need to use a lock. So I know it's a little bit annoying. Depending on the language that you're implementing your low-level design interview in,
you may have an atomic primitive, you may not. How do you know when to use each? When do you use an atomic variable, and when do you use a lock? Well, you're going to want to use an atomic variable when you're updating just a single variable or counter, so things like flags or simple statistics.
But the moment that you have to update two things, the moment that you have to update two separate variables, and they need to stay consistent with each other, something like transferring money between accounts, well then Atomic can't just help you here anymore. You need to rely on the lock.
So to summarize, we had two main patterns that show up in correctness. We have check then act, and then we have read, modify, write. Both have the same root cause, a gap between operations where another thread can interfere,
and the fix is the same for both. Make the operation atomic, usually with a lock, but sometimes you can use an atomic variable, depending on the language and depending on whether or not you're updating just a single variable. The second category of concurrency problems is what's called coordination.
Let's say that you are building a website and users sign up and you need to send them a welcome email. But sending an email takes about 500 milliseconds, half a second, and you don't want the sign
up request to be blocked for that whole half second. So you basically want to hand off that expensive work, you want to hand off the sending of email to a background thread and then just return the sign-up request to the user immediately. So you have these API threads that need to pass tasks to email sender or worker threads.
And the natural solution is to just put a queue between the two of them. So the producer pushes tasks into the queue and then the consumers or the email senders can just pull things off of the queue and send those emails as they see fit.
This seems simple, but in doing so you've actually introduced two problems. The first is how does this worker know when new work has arrived? Let me write that. How does consumer know that work arrived?
And how does it know so efficiently? We'll talk about this in just a moment. And then the second is what happens if work is arriving too fast for a consumer to keep up? You can imagine that we have tons of send emails here.
We only have a couple worker threads And this queue just keeps getting larger and larger and larger So those are the two problems that we need to solve and let go ahead and start with the first one So basically your worker thread needs to be able to wait for tasks
And now the naive approach is that your thread is just going to spin in a loop always checking if there is work. And so you can see here we'll just say while true, if the queue is empty then try to pull off the queue and process it back.
And this is just going to keep processing while true basically non-stop. You can see again our Java and our Python implementation. But there's a clear issue with this, and that's that we're just burning CPU doing nothing.
This thread is fully occupied by just doing that, looping and looping and looping, always checking the queue, are you ready, are you ready, are you ready, are you ready? And this is wasting valuable CPU resources that we could be using elsewhere. So the first solution that you might have thought of is,
well, we could sleep and pull. Basically, let's introduce a sleep here so that we're not just, but maybe we're waiting just a little bit. And so that would look something like this. Let me put it in here for Java.
In this case, we're going to try to pull. If we didn't get it, then we're going to sleep for just 100 milliseconds, and then we'll try again. This will make sure that we're wasting less CPU. I'll paste in here the same thing for Python so you can see what that would look like.
Now, this might be better. You're wasting fewer cycles, or you're still wasting cycles, but you've also introduced latency. And so now if a new job comes in to send an email that should take 500 milliseconds, it could take 600 milliseconds because it could land and we have to wait this 100 milliseconds
before we're even able to pull it off of the queue. What you actually want is for the workers to sleep when there's nothing to do and then wake up instantly the moment that that work arrives. The solution is to use what's called a blocking queue. And just like with locks and just like
with atomic variables, most major languages have a blocking queue built in. The way that it works is the worker calls a function called take. And if they call take on the empty queue, and that thread actually just goes to sleep.
And then when a producer calls put to put something into the queue, it automatically wakes up one of the sleeping workers saying there's something for you to process now. So let me paste this in so you can see what the solution looks like starting with Java.
And so in Java, we're going to instantiate, let's ignore that for now, we're going to instantiate a blocking queue. And so when a new user comes in, we're going to put onto that queue and return success. Cool, that thread is done. Now on the other end for our consumer,
while it's still in a while loop, if it calls queue.take and this is empty, it is going to go to sleep at this moment. It's going to say everybody else can use the CPU, I don't need it now. And then it's not until that next put comes in that this is going to automatically wake up, process the task,
and then repeat if it's empty, sleep, and so on. And I can show you what that implementation looks like in Python as well. Same idea. Python's queue is actually a blocking queue by default. Let me remove that for now.
It's actually a blocking queue by default. And so the same thing, you have a put, but instead of take, you have a get in Python. So a slightly different API, but the same general concept. So now let's come back to what that second issue was within coordination.
What happens if a worker is arriving, if work is arriving too fast for the consumers to be able to keep up? You could say, maybe there's marketing emails that just went out, 50,000 users just clicked on the link all at once.
This generates a bunch of background tasks, a bunch of emails to be sent out. The queue grew and grew and grew. it's eventually unbounded and as a result we run out of memory and the entire process crashes. That's what we're trying to avoid here. And so the solution is to use a bounded blocking queue.
You basically set a maximum size and when it's full, producers block until there's bloom. This is what's called back pressure. Very important. So the system naturally slows down when it can't keep up. And the blocking queue will solve this problem for us. And the implementation
of a blocking queue is really straightforward. You actually saw me remove it because I was a a little ahead of myself, but it's just that when you instantiate these blocking cues, you can instantiate them with the maximum number of items that you want in the cue before we start to apply some back pressure, and that will naturally make it a bounded
blocking cue. So you can see that in the case there, and then Python, and we already removed it, but it was that max size, 1000 parameters. And using a blocking cue, specifically a bounded blocking cue, you're going to almost
always want to make sure that you bound it. I see no reason why you would ever not bound it. But this is going to show up in interviews whenever you need to process something asynchronously. So, task schedulers, background job processor, message queues. If work is pulling in from one component to another, from one thread specifically to another,
then that's coordination. And the blocking queue is almost always your answer. There's more to it, of course. How do you size the buffer appropriately? What happens during shutdown? What if blocking isn't acceptable on your request task?
The main thing to remember for your interview is that if you need coordination, then you're going to want to use a bounded blocking queue. The final category of concurrency problems is called scarcity. And this is when you need to limit concurrent access to finite resources.
So for example, I want you to say that you're calling an external API that only allows 10 concurrent requests, that's its rate limit. And you have 50 threads that might all be trying to call it simultaneously.
You need some way to say only 10 of you can be here at any given time. The tool for this is what's called a semaphore. You probably remember semaphores, I'm sure you learned them in school. And you can think of semaphores conceptually as just a bucket of permits.
Before you do some operation, you grab a permit out of the bucket, and once you finish that operation you put the permit back. If you arrive and there are no permits in the bucket, then you need to wait until somebody comes and replenishes the permit So let look at the implementations and again Java and Python In Java you have a built semaphore Just like the others it present in most languages
And in this case, let's say that you're downloading some expensive file and you want to make sure that you're only downloading five files at the same time. Maybe you don't want to run out of memory, or you don't want to run out of I.O. And so you first acquire that permit.
You basically grab the permit out of the bucket. You do that expensive download, and then you release the permit. And this is going to guarantee you that only five threads can be doing this download at the same time. Exact same concept in Python. You have the semaphore, you acquire, and you release, respectively.
Now, here's one interesting thing to call out. What happens if this do download through an exception? If this were to through or through an exception, then we would break out of this download function, the exception would bubble up, and we would never release or return the semaphore,
and we would never return that permit. If that happens five times, then your bucket of permits, to continue with this analogy, would be completely empty. None of your semaphores would have released because they all threw, and they'd be holding
onto that semaphore forever. This would bring everything to a screeching halt, and there would be no more downloads. This is a pretty common bug that you want to look out for in your interviews whenever you're dealing with scarcity, is can I guarantee that this semaphore is going to be put back,
that this permit is going to be put back so that others can use it? And the solution, fortunately, is really straightforward. You just want to wrap this in a try cache with a finally. So you're going to try to do download.
If it fails or anything goes wrong at all, then the permit is going to be released. So we can do that exact same thing, of course. In Python, you all know probably what this looks like. But for completeness, it'll look just like that. Now, sometimes it's the case that you're not just counting.
You're not just counting the number of requests. But you're actually managing actual objects that have states. Start with me. This is pretty important. So the example here would be a database connection. So the classic example. Each connection holds an OpenTCP socket.
It consumes memory. It tracks transition state. And so you can't create a new one for every single request or every single query you want to hit the database with. Instead, you create a connection pool. You create maybe 10 open connections,
and then all your threads can reuse those 10 open connections so we're not having to open and close connections all the time. And this is a different flavor of a scarcity problem. In this case, we're just counting. In the case that I'm describing now, you have a fixed set of resources.
And the solution is the same that we saw in coordination. You can use a blocking queue. You fill it up with your objects, threads take one out, use it, and then put it back. So it's the same underlying primitive, a blocking queue, but we're not using it in quite the same way.
We're basically using it as like a bag of objects or a pool of objects that can be reused. Let me show you exactly what that would look like. So let me just write this. When managing actual objects with state.
So here's the job implementation. Again, we have our blocking queue. we should always instantiate it with a limit. And then you can load up your connections. So maybe we have 10 connections.
So we put 10 connections into our pool. Those are now in the queue. And every time you want to run a query, the first thing that you're going to do is take from the pool. Of course, if none are ready, then we're just blocked and we're waiting, as you remember, from coordination.
We're going to run our query. And then finally, we're going to put it back into the pool. So as opposed to releasing locks or putting the semaphore back, we're going to just put the connection back into the pool, basically call input again,
and the next thread that is waiting can pick up at that point. I'll show you, of course, what it looks like in Python 2. Exact same idea there. The pattern of these two scarcity problems is the same either way.
You acquire something scarce, you use it, you then release it, and you make sure that release happens even when things fail by using that finally block. All right, so let's zoom back out here as we wrap things up.
If you find yourself in a low-level design review and concurrency comes up, here's your mental checklist. The first thing you want to ask yourself is that is there a shared state that multiple threads can access? If the answer is yes, then you're talking about a correctness issue.
And for correctness issues, we talked about both read, modify, write, as well as check and act. And in either case, you want to use either a lock or atomic variables. The next thing you want to consider is, is work flowing from one thread to another?
If so, then that's a coordination problem. And so you think about what happens when there's no work and what happens when there's too much work. And in both cases, you probably want to use a blocking queue or bounded blocking queue.
And then third, we talked about if there was a fixed limit of something, then it's scarcity. And so make sure that resources always get released, of course, in that final block. And then you probably want to reach for either a semaphore
or a blocking queue to be your pool, as we discussed. Now, most interview problems, they map cleanly to one of these three categories. And so once you're able to recognize these three,
then you can see them in the interview and you know exactly which solutions to apply and things end up being pretty simple. So hopefully this was valuable. As I had mentioned, we have full written breakdowns on each of these correctness, coordination, and scarcity
with lots more examples in each of the core programming languages, what you should look out for in an interview, the corner cases, far more detail than I've gone into in this video. That's going to be linked in the description that you can check out. Unfortunately, that's only for premium users. And as always, if you like the content, like and subscribe.
We'll be back with more soon. Take care.
