Skip to content
LLD Learn/Concurrency for LLD
Browsing as a guest. Sign in to save your progress and earn XP as you complete chapters.

Concurrency-Safe Patterns

6 min read

You'll learn to

  • -Adapt Builder and Object Pool for safe use under concurrent access
  • -Explain the trade-off between coarse-grained locking (simple, slower) and fine-grained locking (faster, easy to get wrong)

Most of the patterns covered earlier in this course are inherently safe under concurrency because they produce immutable results or stateless objects. A few, though, need explicit thought when multiple threads use them at once - this chapter covers the two that come up most often as interview follow-ups.

Builder Under Concurrency: Usually a Non-Issue, With One Exception

A Builder instance used entirely within a single thread (construct, configure, build(), discard) needs no special handling - it is local, mutable state that never crosses a thread boundary. The exception is a shared Builder instance reused across threads, which reintroduces exactly the race conditions from earlier chapters: two threads calling .add_topping() on the same builder can interleave unpredictably. The fix is almost always simpler than adding locks: do not share Builder instances across threads. Each thread constructs its own Builder, and only the final, immutable result of build() is ever shared.

Object Pool: Reusing Expensive Objects Safely

Object Pool maintains a set of initialized, reusable objects (database connections, thread instances) rather than constructing and destroying them on every use, since construction is expensive. Under concurrency, the pool itself is a shared, mutable resource - the exact shape flagged in the "why concurrency matters" chapter - so acquiring and releasing objects from the pool needs to be thread-safe.

A thread-safe object pool using a bounded blocking queue
import threading
from collections import deque

class ConnectionPool:
    def __init__(self, size: int):
        self._available = deque(DatabaseConnection() for _ in range(size))
        self._lock = threading.Lock()
        self._connection_released = threading.Condition(self._lock)

    def acquire(self) -> "DatabaseConnection":
        with self._connection_released:
            while not self._available:
                self._connection_released.wait()   # block until one is returned
            return self._available.popleft()

    def release(self, conn: "DatabaseConnection") -> None:
        with self._connection_released:
            self._available.append(conn)
            self._connection_released.notify()      # wake a waiting acquirer

Notice this is the same bounded-blocking-queue mechanism from the previous chapter, applied to a pool of reusable objects instead of a stream of work items - a strong sign of how reusable these underlying concurrency primitives are once you recognize the shape.

A common bug in object pool implementations: forgetting to release a connection back to the pool on an exception path. A try/finally (or a context manager, in Python) around acquire/release is worth calling out explicitly - a leaked connection permanently shrinks the pool.

Interview Signal is part of Pro

See a real weak answer next to a real strong one for this exact topic.

Quiz is part of Pro

Test what you just read with a short quiz, and bank the XP.

ScaleDojo Logo
Initializing ScaleDojo