Concurrency & System Coding
- Hand-roll a job scheduler, a bounded blocking queue, and a resource limiter
- Get each problem's sequential core right before wrapping it in locks and conditions
- Rehearse the details Rubrik grades: while-loop waits, finally blocks, interrupts, shutdown
Rubrik's "System Coding" round is runnable, low-level, and overwhelmingly about concurrency. You are handed a systems-flavored prompt on CoderPad, you write code against it, and you compile and run it, so the bar is working thread-safe machinery, not whiteboard pseudocode. The recurring twist is a ban on the convenient tools: the interviewer forbids ExecutorService, BlockingQueue, and the concurrent collections, then asks you to build the equivalent from a lock and a condition variable. This lesson works the three families that recur, a dependency job scheduler, a producer-consumer buffer, and a resource limiter, each stripped to the core logic first and then wrapped in the real concurrency.
InterruptedException means and where you catch it, and how a clean shutdown() drains in-flight work. Build that base cold and the novel scheduler or allocator becomes a variation on machinery you already own.The dependency scheduler
Scheduling dominates the concurrency family. The recurring shape is a job scheduler that runs tasks respecting a dependency graph. Task B cannot start until task A finishes, several independent tasks may run at once, and on any failure the whole run stops. Before you touch a thread, the question underneath is pure graph work. Which jobs are runnable now, which unlock once those finish, and is the graph even schedulable or does a cycle make it impossible. That resolution into ordered waves is deterministic, so it is the piece the widget can test. The parallel execution that consumes the waves is the layer you add in code.
Job Scheduler: Resolve the Dependency Graph into Parallel Waves
Problem
n jobs labeled 0 to n - 1 and a list deps where deps[i] = [job, prereq] means job cannot start until prereq has finished. Resolve the graph into ordered execution waves: wave 0 is every job with no unfinished prerequisites, wave 1 is every job whose prerequisites all complete in wave 0, and so on. Every job in a wave can run in parallel because none depends on another in the same wave.Return the list of waves, each wave a list of job ids sorted ascending. If the dependencies contain a cycle so that some job can never run, return an empty list.
Input: n = 3, deps = [[1,0],[2,0]]
Output: [[0],[1,2]]
Job 0 has no prerequisite, so it runs first. Jobs 1 and 2 both wait only on 0, so they run together in the next wave.
Input: n = 2, deps = [[0,1],[1,0]]
Output: []
Job 0 waits on 1 and job 1 waits on 0, a cycle, so neither can ever start.
This is a level-by-level topological sort (Kahn's algorithm). Count each job's unfinished prerequisites as an in-degree. The current wave is every job at in-degree zero; process it, decrement the in-degree of everything it unlocks, and the newly-zero jobs form the next wave. If you never process all n jobs, a cycle remains.
Job Scheduler: Resolve the Dependency Graph into Parallel Waves
Click "Run" to execute your code
Test Cases
[3,[[1,0],[2,0]]]Expected: [[0],[1,2]][1,[]]Expected: [[0]][2,[[0,1],[1,0]]]Expected: [][4,[[1,0],[2,0],[3,1],[3,2]]]Expected: [[0],[1,2],[3]][3,[]]Expected: [[0,1,2]][5,[[1,0],[2,0],[3,1],[3,2]]]Expected: [[0,4],[1,2],[3]]With the waves in hand, the concurrent executor is a loop over them. It dispatches a wave's jobs to a pool of worker threads, blocks until that wave drains, and then releases the next. The blocking, the completion counting, and the stop-on-failure are the parts the interviewer watches, and the ban on built-ins means you build the pool and the wait yourself.
// waves comes from the deterministic resolver above.// pool is a hand-rolled fixed thread pool (see the bounded-queue section).void runWaves(List<List<Integer>> waves, ThreadPool pool, Map<Integer, Runnable> jobs) { for (List<Integer> wave : waves) { final int size = wave.size(); final int[] done = {0}; // completed jobs in this wave final ReentrantLock lock = new ReentrantLock(); final Condition waveDrained = lock.newCondition(); for (int id : wave) { pool.submit(() -> { try { if (!failed) jobs.get(id).run(); } catch (RuntimeException e) { failed = true; // one throw stops the run } finally { lock.lock(); try { done[0]++; if (done[0] == size) waveDrained.signal(); } finally { lock.unlock(); // release even if run() threw } } }); } lock.lock(); try { while (done[0] < size) waveDrained.await(); // while, not if } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore the flag, do not swallow return; } finally { lock.unlock(); } if (failed) break; // do not launch the next wave }}The bounded blocking queue
The thread pool that ran those waves needs a queue between the submitter and the workers, and Rubrik's ban on BlockingQueue means you write it. A bounded queue is the canonical producer-consumer primitive: producers add work, consumers take it, the buffer has a fixed capacity, and the interesting moments are the boundaries. A producer that finds the buffer full must wait, and a consumer that finds it empty must wait. Those waits are the concurrency. Strip them away and what remains is a fixed-capacity FIFO whose enqueue and dequeue semantics are entirely deterministic, so that core is what the widget tests.
Bounded Buffer: Model the Producer-Consumer Semantics
Problem
BoundedBuffer(capacity) with three operations:-
offer(x): add x if there is room, returning true. If the buffer is already full, change nothing and return false, which is the point where a real producer thread would block on the not-full condition.-
poll(): remove and return the oldest item in first-in-first-out order. Return null when the buffer is empty, the point where a real consumer would block on not-empty.-
size(): return the current number of items.Modeling each blocking boundary as a returned value lets you prove the buffer's invariant before wrapping it in a lock and two condition variables.
Input: ops = [offer(1), offer(2), offer(3), size(), poll(), offer(3), poll(), poll(), poll()], capacity = 2
Output: [null, true, true, false, 2, 1, true, 2, 3, null]
The third offer fails because capacity is 2 and the buffer is full. Items leave oldest-first, and the final poll on an empty buffer returns null. The leading null is the constructor.
Input: ops = [offer(1), size(), poll(), size()], capacity = 0
Output: [null, false, 0, null, 0]
A zero-capacity buffer refuses every offer and always polls null.
Hold the capacity and a list acting as the queue. offer compares the length against capacity. Room means append and return true; full means change nothing and return false. poll removes and returns the front element, or null when empty. size is the length. Everything reduces to those two boundary checks.
Bounded Buffer: Model the Producer-Consumer Semantics
Click "Run" to execute your code
Test Cases
[["BoundedBuffer",[2]],["offer",[1]],["offer",[2]],["offer",[3]],["size",[]],["poll",[]],["offer",[3]],["poll",[]],["poll",[]],["poll",[]]]Expected: [null,true,true,false,2,1,true,2,3,null][["BoundedBuffer",[1]],["poll",[]],["offer",[9]],["offer",[8]],["poll",[]],["size",[]]]Expected: [null,null,true,false,9,0][["BoundedBuffer",[3]],["offer",[10]],["offer",[20]],["offer",[30]],["poll",[]],["poll",[]],["poll",[]]]Expected: [null,true,true,true,10,20,30][["BoundedBuffer",[0]],["offer",[1]],["size",[]],["poll",[]]]Expected: [null,false,0,null][["BoundedBuffer",[2]],["offer",[1]],["poll",[]],["offer",[2]],["offer",[3]],["offer",[4]],["poll",[]],["poll",[]]]Expected: [null,true,1,true,true,false,2,3]return false with a producer waiting on a not-full condition, and return null with a consumer waiting on not-empty. Guard both with a single lock, wait inside a while loop that rechecks the predicate after each wakeup, and signal the opposite condition after every state change. Decide deliberately how put and take propagate InterruptedException rather than swallowing it.The concurrent buffer keeps those same two checks and turns each boundary into a wait. One lock guards the buffer so the size check and the mutation cannot interleave. A not-full condition parks a producer while the buffer is full; a not-empty condition parks a consumer while it is empty. Each side signals the other after it changes the buffer. Both waits sit inside a while loop, because a signal wakes a thread without promising the predicate still holds when it runs.
class BlockingBuffer<T> { private final Deque<T> buf = new ArrayDeque<>(); private final int capacity; private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); BlockingBuffer(int capacity) { this.capacity = capacity; } void put(T x) throws InterruptedException { lock.lock(); try { while (buf.size() == capacity) notFull.await(); // while, so a spurious wake rechecks buf.addLast(x); notEmpty.signal(); // a taker may now proceed } finally { lock.unlock(); // released even if await threw } } T take() throws InterruptedException { lock.lock(); try { while (buf.isEmpty()) notEmpty.await(); T x = buf.removeFirst(); notFull.signal(); // a putter may now proceed return x; } finally { lock.unlock(); } }}The grading lives in the details that are invisible when you lean on an imported queue. Wait in a while loop, never an if, because a thread can wake spuriously or lose the race for the item it was signaled about. Release the lock in a finally so a thrown exception cannot strand it. Signal the correct condition after each state change so a full or empty buffer does not park every thread forever. And propagate InterruptedException deliberately, either declaring it or restoring the interrupt flag, rather than catching and discarding it. These are the exact places interviewers probe, and the deterministic core you proved above is what lets you get them right without also debugging the FIFO logic.
Resource allocation and rate limiting
The other pole of the concurrency family is resource allocation: bound how many threads may hold a resource, or how fast they may consume it. The named puzzles here are the "Political Bathroom" (one room, at most three people, never a mix of the two parties, fair queueing with no starvation) and a shared playground capped at ten players. There is also a searcher-inserter-deleter that allows concurrent reads but exclusive writes. Each is really a question about which synchronization construct fits. The runnable core in this section is a rate limiter, whose admit-or-deny decision on a stream of timestamps is fully deterministic, and the construct discussion follows it.
Token-Bucket Limiter: The Deterministic Admit-or-Deny Decision
Problem
capacity tokens and starts full. It gains refillRate tokens per unit of time, never exceeding capacity. Each request costs one token: if at least one token is available the request is admitted and a token is spent, otherwise it is denied and nothing changes.You are given
capacity, refillRate, and a chronological list times of request arrival times (non-decreasing integers). Return a list of booleans, one per request, where true means admitted and false means denied.Input: capacity = 2, refillRate = 1, times = [0,0,0,2,2]
Output: [true, true, false, true, true]
The bucket starts with 2 tokens, so the first two same-instant requests pass and the third is denied. By time 2 it has refilled to 2 tokens (capped), so both requests at time 2 pass.
Input: capacity = 3, refillRate = 0, times = [0,0,0,0]
Output: [true, true, true, false]
With no refill, only the three starting tokens are ever available.
Carry a token count and the time of the last request. On each request, add refillRate tokens for the elapsed time and cap at capacity; then admit and spend a token if at least one is available, else deny. Because refillRate and the times are integers, the token count stays an integer and the decisions are exact.
Token-Bucket Limiter: The Deterministic Admit-or-Deny Decision
Click "Run" to execute your code
Test Cases
[2,1,[0,0,0,2,2]]Expected: [true,true,false,true,true][3,0,[0,0,0,0]]Expected: [true,true,true,false][1,1,[0,1,2]]Expected: [true,true,true][1,1,[0,0,0]]Expected: [true,false,false][2,2,[0,0,0,1]]Expected: [true,true,false,true][5,1,[]]Expected: []Made concurrent, the limiter's whole critical section is the refill-then-spend step: two threads must not both observe the last token and both spend it. One lock around that step is enough for a single process. The resource-allocation puzzles differ only in which construct enforces the bound, and naming the right one is itself part of the score.
// Single-process limiter: refill and spend cannot interleave.boolean tryAcquire(long now) { lock.lock(); try { tokens = Math.min(capacity, tokens + (now - last) * refillRate); last = now; if (tokens >= 1) { tokens -= 1; return true; } // admit return false; // deny } finally { lock.unlock(); }}// Playground capped at ten: a semaphore is the natural fit for a counted resource.Semaphore slots = new Semaphore(10, true); // fair, so no player starvesvoid enter() throws InterruptedException { slots.acquire(); // blocks past the tenth player}void leave() { slots.release(); // frees a slot for a waiter}The details that decide it
The System Coding round is medium-to-hard in raw algorithmic terms but hard in execution. The algorithm under a scheduler or a blocking queue is not exotic. The hard part is producing it as correct, runnable, thread-safe code with the right primitives and clean exception handling, on the first real attempt and under time. Because CoderPad runs the code, the interviewer sees whether it compiles, whether it deadlocks, and whether it races, so a plain implementation that survives execution outscores an ambitious design that does not quite work.
if instead of a while invites a lost or spurious wakeup. Failing to release the lock in a finally strands it on an exception. Signaling the wrong condition, or forgetting to signal after a state change, parks threads forever. Swallowing InterruptedException instead of declaring it or restoring the flag hides a cancellation. Reaching for a banned built-in when asked to hand-roll the primitive misses the entire point of the round. Prove the deterministic core, then wrap it while watching each of these.Common questions
Why can these problems be tested as deterministic when the round is about concurrency?
Every concurrency problem has a sequential core: the dependency order a scheduler resolves, the FIFO semantics of a bounded buffer, the admit-or-deny arithmetic of a limiter. That core is deterministic, and it is the piece you should prove correct before any lock goes in. The concurrency layer sits on top: the locks and condition variables that turn boundaries into waits. A fixed input-to-output test cannot check it, so the static Java blocks in this lesson show it instead. On CoderPad the interviewer runs the real concurrent version and watches for races directly.
Which synchronization construct should I reach for?
Let the problem shape pick it. A semaphore caps a counted resource, so it fits a player limit or a token count. A condition variable fits "wait until this predicate holds," so it fits a blocking queue's full and empty boundaries. A read-write lock fits concurrent reads with exclusive writes, so it fits the searcher-inserter-deleter. A cyclic barrier fits "everyone waits at the same checkpoint." Say why your pick beats the alternatives in a sentence, because the justification is part of the signal Rubrik grades.
What language should I use, and do I need C++?
Java is the default and the safest choice, since its lock, condition, and semaphore primitives map directly onto what interviewers ask you to build. C++ and Python both appear and the rounds are language-flexible, so use the one whose threading model you know best. Command of your language's memory and threading model matters far more than the choice itself, because the grading is on correct concurrent code, not on a specific syntax.
How do I handle the "no built-in concurrent utilities" ban?
Expect it and prepare for it. Say ExecutorService, BlockingQueue, and the concurrent collections are off the table. You build the thread pool from worker threads pulling off a hand-rolled bounded queue. The queue comes from a lock and two condition variables, and the counted limits from a semaphore or a guarded counter. The families in this lesson compose: the scheduler's executor uses the pool, the pool uses the blocking queue, and the limiter uses the same lock discipline. Learn the primitives once and you can assemble whatever the prompt asks.
Key Takeaways
- ✓Rubrik's System Coding round is runnable and concurrency-first, and the built-in utilities are frequently banned. Every problem has a deterministic core you should prove first, then wrap in locks and condition variables, which is the split this lesson works three times.
- ✓The dependency job scheduler resolves into parallel waves with a level-by-level topological sort: in-degree-zero jobs form the current wave, finishing them unlocks the next, and an emitted count short of n reveals a cycle. The concurrent executor dispatches each wave to a hand-rolled pool and stops on the first failure.
- ✓The bounded blocking queue is a fixed-capacity FIFO whose full and empty boundaries become waits. Guard it with one lock, wait inside a while loop, signal the opposite condition after each state change, and release the lock in a finally so an exception cannot strand it.
- ✓Resource limits are a construct-choice question: a semaphore for a counted resource, a condition variable for a predicate wait, a read-write lock for concurrent reads with exclusive writes. Name the tradeoff aloud, handle InterruptedException deliberately, and keep the critical section as small as the refill-then-spend step.
Rubrik interview guideKnow what Rubrik asks before you walk in.
11 coding · 3 system design · 15 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.