A thread-safe LRU cache

System design scenarios Senior Swift 6.0+ iOS 18+ Reviewed By Alok Singh

Predict the output

A cache with capacity 2 runs these calls. Which key is evicted?

cache.insert(1, for: "a")
cache.insert(2, for: "b")
_ = cache.value(for: "a")
cache.insert(3, for: "c")
Options
Hint 1 of 1

Which key was used least recently?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: B b

Correct. After a is read, b is the least recently used.

Why not the others?
  • A a. Reading a makes it the most recently used.
  • C c. The new item is the most recent.
  • D Nothing, because the cache grows. Capacity is 2, so one item must go.

30-second answer

Combine a dictionary from key to node with a doubly linked list ordered by recency, so lookups, inserts and evictions are O(1). On a hit, move the node to the front; on insert past capacity, remove the tail. Protect the state with a Mutex (iOS 18) or an actor, depending on whether callers are synchronous or async.

Practise saying it · 60 s. No recording, just a timer

Explain your LRU design.

Say your answer out loud, then check which of these points you covered:

  • Dictionary + linked list
  • O(1) operations
  • Reads mutate
  • Mutex or actor

Deep dive

Make back pointers weak (or manage them carefully) to avoid reference cycles.

An actor version is simpler, but every call becomes async.

NSCache isn't LRU and evicts unpredictably, but handles memory pressure.

Add a cost limit if values vary in size.

Mutex-backed LRU cache Type-checked · Swift 6.4
import Synchronization

final class LRUCache<Key: Hashable & Sendable, Value: Sendable>: Sendable {
    private final class Node {
        let key: Key
        var value: Value
        weak var prev: Node?
        var next: Node?
        init(key: Key, value: Value) { self.key = key; self.value = value }
    }

    private struct State {
        var map: [Key: Node] = [:]
        var head: Node?          // most recently used
        var tail: Node?          // least recently used

        mutating func remove(_ node: Node) {
            if let prev = node.prev { prev.next = node.next } else { head = node.next }
            if let next = node.next { next.prev = node.prev } else { tail = node.prev }
            node.prev = nil; node.next = nil
        }
        mutating func pushFront(_ node: Node) {
            node.next = head
            head?.prev = node
            head = node
            if tail == nil { tail = node }
        }
    }

    private let state = Mutex(State())
    let capacity: Int

    init(capacity: Int) {
        precondition(capacity > 0)
        self.capacity = capacity
    }

    func value(for key: Key) -> Value? {
        state.withLock { s in
            guard let node = s.map[key] else { return nil }
            s.remove(node); s.pushFront(node)
            return node.value
        }
    }

    func insert(_ value: Value, for key: Key) {
        state.withLock { s in
            if let node = s.map[key] {
                node.value = value
                s.remove(node); s.pushFront(node)
                return
            }
            let node = Node(key: key, value: value)
            s.map[key] = node
            s.pushFront(node)
            if s.map.count > capacity, let lru = s.tail {
                s.remove(lru)
                s.map[lru.key] = nil
            }
        }
    }
}

Common mistakes

  • O(n) recency updates with an array.
  • Locking only writes, not reads that reorder.

Senior take & follow-ups

Seniors give O(1) operations, explain why reads mutate, and pick the right lock.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why must reads take the lock?

    Show hint

    A read moves the node, which mutates the list.

  2. Mutex or actor?

    Show hint

    Mutex for synchronous callers, actor for async ones.

From the author

DSA Master

Made by the author: practise the data structures and algorithms that sit next to iOS questions in most interview loops, with step-by-step visualizations and runnable code.

Suggest a fix if something here is wrong or out of date.