Swift Data Structures for System Design
Build the five components that appear in every mobile system design answer.
Introduction
30 Second Summary
In system design interviews, you draw boxes labeled "cache layer" or "sync queue" on a whiteboard. In live coding rounds, the interviewer points at one of those boxes and says: "Now implement it." Most candidates can describe the system but freeze when asked to build its internals under time pressure.
In this project, you will build a Swift package containing five production-grade data structure implementations. Each one is a component you would name in a system design answer, now backed by unit tests proving correctness and time complexity.
What You'll Build
You will open the test suite, run it, and walk an interviewer through each implementation. For every component, you will explain the problem it solves, the complexity guarantees it provides, and the alternatives you considered.
By the end of this project, you'll have:
- An LRU Cache with O(1) get, put, and eviction — the component inside every "caching layer" box you draw on a whiteboard. You can insert data, watch the least-recently-used entry get evicted, and prove performance with unit tests.
- An indexed query engine that retrieves records by category in O(1) lookup time, filters by date range, and sorts by multiple criteria. This is the data layer behind any filtered list or dashboard.
- A debounced search controller that fires only the final query when rapid input arrives. This is the request deduplication pattern inside every search bar and API client. Tests prove intermediate calls never execute.
- An actor-isolated state manager that handles 100 concurrent mutation requests without data races. This is the thread-safe shared state pattern behind any sync engine or booking system.
- Secret Mission: a cursor-based pagination engine with concurrent prefetching using TaskGroup — the infinite scroll engine behind any feed or list.
Are there any prerequisites?
You need Xcode 16 installed on macOS. This project uses Swift 5.9+ and XCTest for unit testing. Familiarity with Swift basics (structs, classes, protocols, generics, async/await) is expected.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters for your next interview.
Set Up the Swift Package and Domain Model
Every data structure you build needs something concrete to operate on. Without shared domain types, your LRU Cache, index, and actor would each invent their own ad-hoc structures.
In this step you will create a Swift Package called ReservationEngine and define the domain model that all four implementations will share. You will also write your first unit test to prove the model works.
In this step, get ready to:
- Create a Swift Package named ReservationEngine in Xcode.
- Define domain types for reservations, guests, and statuses.
- Verify the package builds and the test passes with swift test.
Create the Swift Package in Xcode
A Swift Package is a standalone module with its own manifest, source targets, and test targets. Creating one gives you a clean workspace with no app boilerplate.
- In Xcode, click File in the menu bar.
- Select New, then Package.
- In the template dialog, select Library and click Next.
- Name the package ReservationEngine.
- Choose your Desktop as the save location and click Create.
What just happened?
Xcode generated a folder structure with Package.swift at the root, a Sources/ReservationEngine/ directory for your library code, and a Tests/ReservationEngineTests/ directory for unit tests.
The generated manifest probably targets a different swift-tools-version than we need. You will replace it with the exact manifest for this project.
The generated Package.swift needs to target swift-tools-version: 5.9 and declare the platform minimums this project requires.
- Open Package.swift in the Xcode editor (it should already be visible in the Project navigator).
- Select all the existing content and replace it with the following:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "ReservationEngine",
platforms: [.macOS(.v13), .iOS(.v16)],
products: [
.library(
name: "ReservationEngine",
targets: ["ReservationEngine"]),
],
targets: [
.target(
name: "ReservationEngine"),
.testTarget(
name: "ReservationEngineTests",
dependencies: ["ReservationEngine"]),
]
)
What does this manifest do?
- swift-tools-version: 5.9 tells SwiftPM which language features and APIs to enable.
- platforms declares macOS 13 and iOS 16 as minimum deployment targets, unlocking modern concurrency APIs.
- The ReservationEngine library product exposes your source target to importers.
- The ReservationEngineTests test target depends on the library so it can import your types.
- Save Package.swift with Cmd+S.
- Open Terminal by pressing Cmd+Space (macOS) and typing Terminal.
- Navigate to your package root by running:
cd ~/Desktop/ReservationEngine
- Verify the manifest is valid by running:
swift build
You should see Build complete! in the terminal output.
Seeing a manifest parsing error?
- Check that the very first line of Package.swift is exactly // swift-tools-version: 5.9 with no leading whitespace.
- Make sure you are in the ReservationEngine directory (run pwd to confirm).
Help me fix a swift build error in my Package.swift manifest.
✔️ Awesome, I've got everything!
Great. Make sure Package.swift is saved before moving on.
ⓧ I'd like to double check the full code
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "ReservationEngine",
platforms: [.macOS(.v13), .iOS(.v16)],
products: [
.library(
name: "ReservationEngine",
targets: ["ReservationEngine"]),
],
targets: [
.target(
name: "ReservationEngine"),
.testTarget(
name: "ReservationEngineTests",
dependencies: ["ReservationEngine"]),
]
)
Define the domain model
The domain model captures the core entities your data structures will manipulate: reservation statuses, full reservations, and guests.
- In Xcode's Project navigator, expand Sources/ReservationEngine/.
- Delete any placeholder file Xcode created in that folder (you may see a file like ReservationEngine.swift).
- Right-click the ReservationEngine folder and select New File from Template, then choose Swift File.
- Name it Models.swift and click Create.
- Replace any generated content with the status enum by pasting the following code:
import Foundation
public enum ReservationStatus: String, Hashable, Sendable, CaseIterable {
case confirmed
case checkedIn
case checkedOut
case cancelled
}
What does this enum do?
ReservationStatus models the four lifecycle states a hotel booking can be in. Conforming to Hashable lets you use it as a Dictionary key.
Sendable conformance tells the Swift compiler this type is safe to pass across concurrency boundaries, which the actor step will require.
- Below the enum's closing brace, add the Reservation struct by pasting this code:
public struct Reservation: Identifiable, Hashable, Sendable {
public let id: UUID
public var guestName: String
public var propertyName: String
public var checkIn: Date
public var checkOut: Date
public var status: ReservationStatus
public init(
id: UUID = UUID(),
guestName: String,
propertyName: String,
checkIn: Date,
checkOut: Date,
status: ReservationStatus = .confirmed
) {
self.id = id
self.guestName = guestName
self.propertyName = propertyName
self.checkIn = checkIn
self.checkOut = checkOut
self.status = status
}
}
What does this struct do?
Reservation holds everything about a single booking: the guest's name, the property, check-in/check-out dates, and current status.
The custom init provides sensible defaults for id and status so callers only need to supply the booking details. All properties are public because the test target imports this module and needs access.
- Save Models.swift and run a quick build to confirm the types compile:
swift build
You should see Build complete! again with no errors.
- Back in Models.swift, add the Guest struct below the Reservation struct's closing brace:
public struct Guest: Identifiable, Hashable, Sendable {
public let id: UUID
public var name: String
public var email: String
public init(id: UUID = UUID(), name: String, email: String) {
self.id = id
self.name = name
self.email = email
}
}
What does this struct do?
Guest represents a person staying at a property. It is simpler than Reservation because the filtering and caching steps focus on reservations.
- Save Models.swift.
Build failing after adding Guest?
- Make sure Guest is outside the Reservation struct's closing brace. Nested types would change the intended API.
- Verify the import Foundation statement is at the very top of the file.
Help me fix a compile error in my Models.swift domain model.
✔️ Awesome, I've got everything!
Great. Confirm you have saved Models.swift with all three types.
ⓧ I'd like to double check the full code
Your Models.swift file is longer than 30 lines, so review it by confirming these three chunks appear in order:
- The import Foundation statement and ReservationStatus enum with four cases (confirmed, checkedIn, checkedOut, cancelled).
- The Reservation struct with six properties and a custom init providing defaults for id and status.
- The Guest struct with three properties (id, name, email) and a custom init.
Add a test and verify the package
XCTest is the built-in testing framework for Swift packages. A simple test that instantiates your model proves the types compile and the package structure is wired up correctly.
- In the Project navigator, expand Tests/ReservationEngineTests/.
- Open the existing test file (or create ReservationEngineTests.swift if Xcode did not generate one).
- Replace all its content with the following:
import XCTest
@testable import ReservationEngine
final class ReservationEngineTests: XCTestCase {
func testModelCreation() {
let reservation = Reservation(
guestName: "John Smith",
propertyName: "Grand Hotel",
checkIn: Date(),
checkOut: Date().addingTimeInterval(86400 * 3),
status: .confirmed
)
XCTAssertEqual(reservation.guestName, "John Smith")
XCTAssertEqual(reservation.status, .confirmed)
}
}
What does this test do?
- @testable import ReservationEngine gives the test access to your library's public and internal types.
- testModelCreation creates a reservation with a 3-day stay and asserts two properties match expected values.
- If this test passes, it means the domain model compiles, the module imports correctly, and the custom init works as intended.
- Save ReservationEngineTests.swift.
Before you run the tests, what do you expect the output to show? Think about how many tests you have written and whether they should pass.
- Run the full test suite from Terminal:
swift test
You should see output indicating 1 test passed with 0 failures.
Tests failing or module not found?
- If you see no such module 'ReservationEngine', make sure your test file is inside Tests/ReservationEngineTests/ (matching the target name in Package.swift).
- If you see type access errors, confirm all structs and enums in Models.swift are marked public.
- Run swift package clean then swift test again to clear any stale build artifacts.
Help me fix a failing swift test for my ReservationEngine package.
✔️ Awesome, I've got everything!
Great. Your package structure is solid and the domain model is tested.
ⓧ I'd like to double check the full code
import XCTest
@testable import ReservationEngine
final class ReservationEngineTests: XCTestCase {
func testModelCreation() {
let reservation = Reservation(
guestName: "John Smith",
propertyName: "Grand Hotel",
checkIn: Date(),
checkOut: Date().addingTimeInterval(86400 * 3),
status: .confirmed
)
XCTAssertEqual(reservation.guestName, "John Smith")
XCTAssertEqual(reservation.status, .confirmed)
}
}
Your domain model is in place and every type compiles cleanly. Next up, you will build an LRU Cache on top of these types to demonstrate O(1) get and eviction.
Build the LRU Cache
Your domain model is in place. Now you need to solve the first real problem: a hotel booking app loads property images and reservation details repeatedly as the user navigates between screens.
A cache keeps frequently accessed data in memory. But when memory fills up, you need deterministic eviction. An LRU Cache guarantees that the least-recently-accessed item is always the one evicted. You will implement one using a Dictionary for O(1) lookups paired with a Doubly Linked List for O(1) reordering.
In this step, get ready to:
- Implement a generic LRUCache class with O(1) get, put, and eviction.
- Write unit tests proving eviction and promotion behavior.
- Add interview-ready comments documenting complexity and NSCache trade-offs.
Create the cache skeleton with Node and properties
The LRU Cache relies on two cooperating data structures. A Dictionary gives O(1) key lookups. A Doubly Linked List gives O(1) insertion, removal, and reordering so you always know which item is least-recently-used (the tail).
- Create a new file Sources/ReservationEngine/LRUCache.swift in Xcode.
- Add the following code to define the class skeleton with its internal Node type, storage properties, and initializer:
import Foundation
public final class LRUCache<Key: Hashable, Value> {
private final class Node {
let key: Key
var value: Value
var prev: Node?
var next: Node?
init(key: Key, value: Value) {
self.key = key
self.value = value
}
}
private var dictionary: [Key: Node] = [:]
private var head: Node?
private var tail: Node?
private let capacity: Int
public var count: Int { dictionary.count }
public init(capacity: Int) {
precondition(capacity > 0, "Capacity must be greater than 0")
self.capacity = capacity
}
}
What does this code do?
Node is a doubly-linked list element. Each node stores a key (for dictionary removal during eviction), a value, and prev/next pointers.
The dictionary maps keys to their corresponding nodes for O(1) lookups. head points to the most-recently-used item. tail points to the least-recently-used item (the eviction candidate).
- Save LRUCache.swift.
- Confirm the skeleton compiles by running this in Terminal from the package root:
swift build
You should see Build complete! with no errors.
Seeing a compile error?
Check that the file is inside Sources/ReservationEngine/ (not the Tests folder). Also confirm the angle brackets in LRUCache<Key: Hashable, Value> are typed correctly.
Help me fix my LRUCache compile error
Add the linked list helper methods
The linked list needs four private operations: add a node to the head, remove any node, move an existing node to the head, and remove the tail. Each operates in O(1) time because you only update pointer references.
- In LRUCache.swift, add the following methods inside the class (before the closing }):
private func addToHead(_ node: Node) {
node.prev = nil
node.next = head
head?.prev = node
head = node
if tail == nil {
tail = node
}
}
private func removeNode(_ node: Node) {
let prev = node.prev
let next = node.next
prev?.next = next
next?.prev = prev
if node === head { head = next }
if node === tail { tail = prev }
node.prev = nil
node.next = nil
}
private func moveToHead(_ node: Node) {
guard node !== head else { return }
removeNode(node)
addToHead(node)
}
private func removeTail() -> Node? {
guard let tailNode = tail else { return nil }
removeNode(tailNode)
return tailNode
}
What does this code do?
- addToHead wires a node as the new head (most-recently-used position).
- removeNode splices any node out of the list by re-linking its neighbors.
- moveToHead combines remove + add to promote a node on access.
- removeTail pops the least-recently-used node for eviction.
Implement the get and put public API
These two methods are what consumers call. get retrieves a value and promotes it to most-recently-used. put inserts or updates, then evicts the tail if the cache exceeds capacity.
- In LRUCache.swift, add the following methods inside the class after the init (before the private helpers you just added):
public func get(_ key: Key) -> Value? {
guard let node = dictionary[key] else { return nil }
moveToHead(node)
return node.value
}
public func put(_ key: Key, _ value: Value) {
if let existingNode = dictionary[key] {
existingNode.value = value
moveToHead(existingNode)
} else {
let newNode = Node(key: key, value: value)
dictionary[key] = newNode
addToHead(newNode)
if dictionary.count > capacity {
if let evicted = removeTail() {
dictionary[evicted.key] = nil
}
}
}
}
What does this code do?
- get looks up the node via the dictionary (O(1)), promotes it to head so it won't be evicted soon, and returns the value.
- put either updates an existing entry or inserts a new one at the head. If the dictionary exceeds capacity, it evicts the tail node and removes it from the dictionary.
- Save LRUCache.swift.
- Confirm the full implementation compiles by running:
swift build
You should see Build complete! again with no errors.
Build failing after adding get and put?
Make sure get and put are inside the class body (between the opening and closing braces of LRUCache). Also confirm the private helper methods are present. get calls moveToHead and put calls addToHead and removeTail.
Help me fix my LRUCache build error
Write unit tests for the cache
The implementation looks correct, but you need to prove it with tests. You will test four behaviors: missing keys return nil, put/get works, eviction targets the correct item, and accessing an item promotes it so it survives eviction.
- Create a new file Tests/ReservationEngineTests/LRUCacheTests.swift.
- Add the test class with two basic tests by pasting the following:
import XCTest
@testable import ReservationEngine
final class LRUCacheTests: XCTestCase {
func testGetReturnsNilForMissing() {
let cache = LRUCache<String, Int>(capacity: 3)
XCTAssertNil(cache.get("nonexistent"))
}
func testPutAndGet() {
let cache = LRUCache<String, Int>(capacity: 3)
cache.put("a", 1)
cache.put("b", 2)
XCTAssertEqual(cache.get("a"), 1)
XCTAssertEqual(cache.get("b"), 2)
}
}
What do these tests prove?
- testGetReturnsNilForMissing confirms that looking up a key that was never inserted returns nil (not a crash).
- testPutAndGet confirms basic insertion and retrieval work for multiple keys.
- Save LRUCacheTests.swift and run the tests:
swift test
You should see 3 tests pass (1 from the original model test, plus your 2 new ones).
- Now add the eviction and promotion tests that prove the LRU behavior. Add these methods inside the LRUCacheTests class (before the closing }):
func testEvictsLeastRecentlyUsed() {
let cache = LRUCache<String, Int>(capacity: 3)
cache.put("a", 1)
cache.put("b", 2)
cache.put("c", 3)
cache.put("d", 4)
XCTAssertNil(cache.get("a"), "LRU item 'a' should have been evicted")
XCTAssertEqual(cache.get("b"), 2)
XCTAssertEqual(cache.get("c"), 3)
XCTAssertEqual(cache.get("d"), 4)
}
func testAccessPromotesItem() {
let cache = LRUCache<String, Int>(capacity: 3)
cache.put("a", 1)
cache.put("b", 2)
cache.put("c", 3)
_ = cache.get("a")
cache.put("d", 4)
XCTAssertNil(cache.get("b"), "Item 'b' should be evicted after 'a' was promoted")
XCTAssertEqual(cache.get("a"), 1)
XCTAssertEqual(cache.get("c"), 3)
XCTAssertEqual(cache.get("d"), 4)
}
func testUpdateExistingKey() {
let cache = LRUCache<String, Int>(capacity: 2)
cache.put("a", 1)
cache.put("a", 99)
XCTAssertEqual(cache.get("a"), 99)
XCTAssertEqual(cache.count, 1)
}
What do these tests prove?
- testEvictsLeastRecentlyUsed inserts 4 items into a capacity-3 cache. The first item ("a") is the LRU and must be evicted.
- testAccessPromotesItem accesses "a" before inserting "d". Now "b" is the LRU (not "a"), so "b" gets evicted instead.
- testUpdateExistingKey confirms that putting the same key again updates the value without increasing count.
- Save LRUCacheTests.swift.
Before you run the tests, predict: will the eviction test pass? Which item do you expect to be evicted when "d" is inserted into a full cache holding "a", "b", "c"?
- Run the full test suite:
swift test
All 6 tests pass (1 model test + 5 LRU cache tests). The eviction test confirms "a" is gone because it was the least-recently-used item at the tail of the list.
Tests failing or not found?
If a test fails with a wrong value, double-check the order of put and get calls. The linked list order depends on insertion sequence. If the test file is not found, confirm it lives at Tests/ReservationEngineTests/LRUCacheTests.swift and has @testable import ReservationEngine at the top.
Help me debug my failing LRU cache tests
✔️ Awesome, I've got everything!
Great. Confirm you have saved LRUCacheTests.swift and all 6 tests pass.
ⓧ I'd like to double check the full code
Scroll up and confirm your LRUCacheTests.swift contains these sections in order:
- The import statements and class declaration.
- testGetReturnsNilForMissing and testPutAndGet (basic tests).
- testEvictsLeastRecentlyUsed, testAccessPromotesItem, and testUpdateExistingKey (eviction and promotion tests).
- The closing } for the class.
Add interview-ready comments
Your implementation works. Now add the comments that let you walk an interviewer through your design decisions without fumbling. These document the complexity guarantees and explain when you would choose this cache over NSCache.
- In LRUCache.swift, find this line at the top of the file:
public final class LRUCache<Key: Hashable, Value> {
- Add the following doc comment block directly above that line:
/// A generic Least-Recently-Used cache with O(1) get, put, and eviction.
///
/// Implementation: Dictionary<Key, Node> for O(1) lookups + Doubly Linked List for O(1) reordering.
/// Space complexity: O(capacity).
///
/// Why not NSCache?
/// - NSCache eviction order is undefined (not LRU).
/// - NSCache purges all objects when the app backgrounds.
/// - NSCache requires NSObject values.
/// Use NSCache when you want automatic memory-pressure eviction and don't care about order.
/// Use LRUCache when you need deterministic, predictable eviction (e.g., image pipeline, API response cache).
Why document NSCache trade-offs here?
In an interview, you will be asked "why not just use NSCache?" Having the comparison right in the code shows you considered the alternative. NSCache is fine when you want the system to manage memory pressure for you. LRU Cache is the right choice when eviction order matters for user experience (e.g., keeping the most-viewed property images cached).
- Save LRUCache.swift.
- Run the test suite one final time to confirm the comments did not introduce any syntax issues:
swift test
All 6 tests still pass. Your LRU Cache is complete and documented.
✔️ Awesome, I've got everything!
Great. Confirm you have saved LRUCache.swift with the doc comment at the top.
ⓧ I'd like to double check the full code
Scroll up and confirm your LRUCache.swift contains these sections in order:
- import Foundation at the top.
- The doc comment block (starting with /// A generic Least-Recently-Used cache) explaining complexity and NSCache comparison.
- The class declaration with the internal Node class, properties, and init.
- The public get and put methods.
- The four private linked list helpers: addToHead, removeNode, moveToHead, removeTail.
- The closing } for the class.
You now have a production-grade LRU Cache with O(1) operations and tests proving its correctness. Next up, you will build an indexed filtering engine that finds reservations by status and date range without scanning the entire collection.
Build the Reservation Index
Your LRU Cache gives you O(1) access to recently viewed reservations. Now you need to solve a different problem: a hotel booking dashboard shows hundreds of reservations, and the user needs to filter them by status, date range, or sort order instantly.
A naive approach using Array.filter with string comparisons is O(n*m). In this step, you will build an indexed data structure that uses Dictionary and Set to achieve O(1) lookups by status and efficient multi-criteria sorting.
In this step, get ready to:
- Implement a ReservationIndex struct with O(1) status filtering using indexed dictionaries.
- Add date range filtering and multi-criteria sorting methods.
- Write unit tests proving correctness of each query method.
Create the ReservationIndex struct with indexed storage
The key insight is maintaining a secondary index alongside the master store. Instead of scanning every reservation to find those with a given status, you keep a pre-built lookup table that maps each status to the set of matching reservation IDs.
- Create a new file Sources/ReservationEngine/ReservationIndex.swift and add the struct with its indexed storage and mutation methods:
import Foundation
/// An indexed collection of reservations enabling O(1) status lookups and efficient multi-criteria sorting.
///
/// Internal indexes:
/// - statusIndex: Dictionary<ReservationStatus, Set<UUID>> for O(1) status filtering
/// - store: Dictionary<UUID, Reservation> as the master record
///
/// Complexity:
/// - filterByStatus: O(k) where k = number of matching reservations (dictionary lookup is O(1), materializing is O(k))
/// - filterByDateRange: O(n) scan but avoids string comparisons
/// - sorted(by:): O(k log k) using Swift's built-in sort with a custom comparator
public struct ReservationIndex {
private var store: [UUID: Reservation] = [:]
private var statusIndex: [ReservationStatus: Set<UUID>] = [:]
public init() {}
public mutating func insert(_ reservation: Reservation) {
store[reservation.id] = reservation
statusIndex[reservation.status, default: []].insert(reservation.id)
}
public mutating func remove(_ reservation: Reservation) {
store[reservation.id] = nil
statusIndex[reservation.status]?.remove(reservation.id)
}
}
What does this code do?
- store is a Dictionary<UUID, Reservation> that holds every reservation keyed by its ID. This is the single source of truth.
- statusIndex is a Dictionary<ReservationStatus, Set<UUID>> that groups reservation IDs by their current status. Looking up a status key is O(1).
- insert adds the reservation to both the master store and the status index simultaneously, keeping them in sync.
- The doc comment at the top documents all complexity guarantees. These are your interview talking points.
The struct currently stores and removes reservations, but has no way to query them. Next you will add the filtering and sorting methods that make the index useful.
- In ReservationIndex.swift, find the closing brace } of the struct. Replace it with the following query methods, then add the closing brace after them:
public func filterByStatus(_ status: ReservationStatus) -> [Reservation] {
guard let ids = statusIndex[status] else { return [] }
return ids.compactMap { store[$0] }
}
public func filterByDateRange(from startDate: Date, to endDate: Date) -> [Reservation] {
store.values.filter { reservation in
reservation.checkIn >= startDate && reservation.checkIn <= endDate
}
}
public func sorted() -> [Reservation] {
Array(store.values).sorted { lhs, rhs in
if lhs.checkIn != rhs.checkIn {
return lhs.checkIn > rhs.checkIn
}
return lhs.guestName < rhs.guestName
}
}
public func filter(status: ReservationStatus, from startDate: Date, to endDate: Date) -> [Reservation] {
guard let ids = statusIndex[status] else { return [] }
return ids.compactMap { store[$0] }.filter { reservation in
reservation.checkIn >= startDate && reservation.checkIn <= endDate
}
}
public var allReservations: [Reservation] {
Array(store.values)
}
}
What does this code do?
- filterByStatus does a single dictionary lookup to get the Set of matching IDs, then materializes the results. The lookup is O(1). Materializing is O(k) where k is the number of matches.
- filterByDateRange scans all stored values comparing Date objects directly. This avoids expensive string parsing and is O(n).
- sorted() uses Swift's built-in sort with a custom comparator: descending by check-in date, then ascending by guest name for ties. This is O(k log k).
- The compound filter(status:from:to:) combines the O(1) status lookup with a date range scan on the smaller subset, avoiding a full table scan.
- Save ReservationIndex.swift.
- Verify the package still compiles by running this in Terminal from your package root:
swift build
You should see Build complete! with no errors.
Seeing a compile error?
- Check that your query methods are inside the struct's closing brace, not after it. The struct should have one } at the very end of the file.
- Verify that Models.swift still exists in Sources/ReservationEngine/. The index references Reservation, ReservationStatus, and UUID from that file.
Help me debug my ReservationIndex compile error.
💡 Why Dictionary + Set instead of Array.filter?
With a plain array of reservations, filtering by status requires scanning every element: O(n). If the dashboard has 500 reservations and the user taps "Checked In", you iterate all 500 every time.
The Dictionary + Set approach pays a small cost on insert (updating the index) but makes every subsequent lookup O(1). For a dashboard that filters frequently but updates rarely, this is the right trade-off to explain in an interview.
Write unit tests for the index
Each query method needs a test that proves it returns the correct subset. You will test status filtering, date range boundaries, and multi-criteria sort order.
- Create a new file Tests/ReservationEngineTests/ReservationIndexTests.swift and add the test class with the status filter test:
import XCTest
@testable import ReservationEngine
final class ReservationIndexTests: XCTestCase {
private func makeDate(daysFromNow days: Int) -> Date {
Calendar.current.date(byAdding: .day, value: days, to: Date())!
}
func testFilterByStatus() {
var index = ReservationIndex()
let r1 = Reservation(guestName: "Alice", propertyName: "Hotel A", checkIn: makeDate(daysFromNow: 1), checkOut: makeDate(daysFromNow: 3), status: .confirmed)
let r2 = Reservation(guestName: "Bob", propertyName: "Hotel B", checkIn: makeDate(daysFromNow: 2), checkOut: makeDate(daysFromNow: 4), status: .checkedIn)
let r3 = Reservation(guestName: "Carol", propertyName: "Hotel C", checkIn: makeDate(daysFromNow: 3), checkOut: makeDate(daysFromNow: 5), status: .confirmed)
index.insert(r1)
index.insert(r2)
index.insert(r3)
let confirmed = index.filterByStatus(.confirmed)
XCTAssertEqual(confirmed.count, 2)
XCTAssertTrue(confirmed.contains(where: { $0.id == r1.id }))
XCTAssertTrue(confirmed.contains(where: { $0.id == r3.id }))
let checkedIn = index.filterByStatus(.checkedIn)
XCTAssertEqual(checkedIn.count, 1)
XCTAssertEqual(checkedIn.first?.id, r2.id)
}
}
What does this test prove?
- makeDate(daysFromNow:) is a helper that creates dates relative to now. This avoids hardcoding dates that become stale.
- The test inserts 3 reservations with different statuses, then asserts that filtering by .confirmed returns exactly the 2 confirmed ones, and .checkedIn returns exactly 1.
- Save the file and run the tests to confirm the status filter works:
swift test
You should see 7 tests pass (your original model test, 5 LRU cache tests, and the new status filter test).
Test not found or failing?
- Make sure the file is inside Tests/ReservationEngineTests/ (not Tests/ directly). The test target only picks up files in that directory.
- Verify the class name matches ReservationIndexTests and the test method starts with test. XCTest only discovers methods prefixed with test.
Help me debug my ReservationIndexTests.
Now add the date range filter test. This verifies that boundary dates (inclusive) are handled correctly.
- In ReservationIndexTests.swift, add this test method inside the class, before the closing brace }:
func testFilterByDateRange() {
var index = ReservationIndex()
let r1 = Reservation(guestName: "Alice", propertyName: "Hotel A", checkIn: makeDate(daysFromNow: 1), checkOut: makeDate(daysFromNow: 3))
let r2 = Reservation(guestName: "Bob", propertyName: "Hotel B", checkIn: makeDate(daysFromNow: 5), checkOut: makeDate(daysFromNow: 7))
let r3 = Reservation(guestName: "Carol", propertyName: "Hotel C", checkIn: makeDate(daysFromNow: 10), checkOut: makeDate(daysFromNow: 12))
index.insert(r1)
index.insert(r2)
index.insert(r3)
let results = index.filterByDateRange(from: makeDate(daysFromNow: 0), to: makeDate(daysFromNow: 6))
XCTAssertEqual(results.count, 2)
XCTAssertTrue(results.contains(where: { $0.id == r1.id }))
XCTAssertTrue(results.contains(where: { $0.id == r2.id }))
}
What does this test prove?
Three reservations check in on days 1, 5, and 10. The query range is day 0 through day 6 (inclusive). Reservations on day 1 and day 5 fall within the range. Day 10 does not. The test asserts exactly 2 results and confirms their identities.
Finally, add the multi-criteria sort test. This uses 5 reservations with overlapping dates to prove the sort order is correct.
- Add this test method below testFilterByDateRange, still inside the class:
func testMultiCriteriaSort() {
var index = ReservationIndex()
let baseDate = Date()
let r1 = Reservation(guestName: "Charlie", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 1), checkOut: baseDate.addingTimeInterval(86400 * 3))
let r2 = Reservation(guestName: "Alice", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 2), checkOut: baseDate.addingTimeInterval(86400 * 4))
let r3 = Reservation(guestName: "Bob", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 2), checkOut: baseDate.addingTimeInterval(86400 * 5))
let r4 = Reservation(guestName: "Diana", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 3), checkOut: baseDate.addingTimeInterval(86400 * 6))
let r5 = Reservation(guestName: "Eve", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 3), checkOut: baseDate.addingTimeInterval(86400 * 7))
index.insert(r1)
index.insert(r2)
index.insert(r3)
index.insert(r4)
index.insert(r5)
let sorted = index.sorted()
XCTAssertEqual(sorted[0].guestName, "Diana")
XCTAssertEqual(sorted[1].guestName, "Eve")
XCTAssertEqual(sorted[2].guestName, "Alice")
XCTAssertEqual(sorted[3].guestName, "Bob")
XCTAssertEqual(sorted[4].guestName, "Charlie")
}
Why this sort order?
The sort is descending by check-in date (most recent first), then ascending by guest name for ties. Day 3 guests (Diana, Eve) come first alphabetically. Day 2 guests (Alice, Bob) come next. Day 1 (Charlie) comes last.
Run all tests and verify the index
Before you run this, how many tests do you expect to pass? Count the original model test, the 5 LRU cache tests, and your 3 new index tests.
- Save ReservationIndexTests.swift and run the full test suite:
swift test
You should see 9 tests pass. All 3 new tests confirm that your index correctly filters by status, handles date range boundaries, and sorts with the multi-criteria comparator.
Tests failing or count is wrong?
- If the sort test fails, double-check that your sorted() method uses lhs.checkIn > rhs.checkIn (descending) for the primary sort. Swapping the comparison direction reverses the order.
- If you see fewer than 9 tests, make sure your new test file is named ReservationIndexTests.swift and is in Tests/ReservationEngineTests/.
Help me debug my failing ReservationIndex tests.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
Here is the complete ReservationIndex.swift:
import Foundation
/// An indexed collection of reservations enabling O(1) status lookups and efficient multi-criteria sorting.
///
/// Internal indexes:
/// - statusIndex: Dictionary<ReservationStatus, Set<UUID>> for O(1) status filtering
/// - store: Dictionary<UUID, Reservation> as the master record
///
/// Complexity:
/// - filterByStatus: O(k) where k = number of matching reservations (dictionary lookup is O(1), materializing is O(k))
/// - filterByDateRange: O(n) scan but avoids string comparisons
/// - sorted(by:): O(k log k) using Swift's built-in sort with a custom comparator
public struct ReservationIndex {
private var store: [UUID: Reservation] = [:]
private var statusIndex: [ReservationStatus: Set<UUID>] = [:]
public init() {}
public mutating func insert(_ reservation: Reservation) {
store[reservation.id] = reservation
statusIndex[reservation.status, default: []].insert(reservation.id)
}
public mutating func remove(_ reservation: Reservation) {
store[reservation.id] = nil
statusIndex[reservation.status]?.remove(reservation.id)
}
public func filterByStatus(_ status: ReservationStatus) -> [Reservation] {
guard let ids = statusIndex[status] else { return [] }
return ids.compactMap { store[$0] }
}
public func filterByDateRange(from startDate: Date, to endDate: Date) -> [Reservation] {
store.values.filter { reservation in
reservation.checkIn >= startDate && reservation.checkIn <= endDate
}
}
public func sorted() -> [Reservation] {
Array(store.values).sorted { lhs, rhs in
if lhs.checkIn != rhs.checkIn {
return lhs.checkIn > rhs.checkIn
}
return lhs.guestName < rhs.guestName
}
}
public func filter(status: ReservationStatus, from startDate: Date, to endDate: Date) -> [Reservation] {
guard let ids = statusIndex[status] else { return [] }
return ids.compactMap { store[$0] }.filter { reservation in
reservation.checkIn >= startDate && reservation.checkIn <= endDate
}
}
public var allReservations: [Reservation] {
Array(store.values)
}
}
And here is the complete ReservationIndexTests.swift:
import XCTest
@testable import ReservationEngine
final class ReservationIndexTests: XCTestCase {
private func makeDate(daysFromNow days: Int) -> Date {
Calendar.current.date(byAdding: .day, value: days, to: Date())!
}
func testFilterByStatus() {
var index = ReservationIndex()
let r1 = Reservation(guestName: "Alice", propertyName: "Hotel A", checkIn: makeDate(daysFromNow: 1), checkOut: makeDate(daysFromNow: 3), status: .confirmed)
let r2 = Reservation(guestName: "Bob", propertyName: "Hotel B", checkIn: makeDate(daysFromNow: 2), checkOut: makeDate(daysFromNow: 4), status: .checkedIn)
let r3 = Reservation(guestName: "Carol", propertyName: "Hotel C", checkIn: makeDate(daysFromNow: 3), checkOut: makeDate(daysFromNow: 5), status: .confirmed)
index.insert(r1)
index.insert(r2)
index.insert(r3)
let confirmed = index.filterByStatus(.confirmed)
XCTAssertEqual(confirmed.count, 2)
XCTAssertTrue(confirmed.contains(where: { $0.id == r1.id }))
XCTAssertTrue(confirmed.contains(where: { $0.id == r3.id }))
let checkedIn = index.filterByStatus(.checkedIn)
XCTAssertEqual(checkedIn.count, 1)
XCTAssertEqual(checkedIn.first?.id, r2.id)
}
func testFilterByDateRange() {
var index = ReservationIndex()
let r1 = Reservation(guestName: "Alice", propertyName: "Hotel A", checkIn: makeDate(daysFromNow: 1), checkOut: makeDate(daysFromNow: 3))
let r2 = Reservation(guestName: "Bob", propertyName: "Hotel B", checkIn: makeDate(daysFromNow: 5), checkOut: makeDate(daysFromNow: 7))
let r3 = Reservation(guestName: "Carol", propertyName: "Hotel C", checkIn: makeDate(daysFromNow: 10), checkOut: makeDate(daysFromNow: 12))
index.insert(r1)
index.insert(r2)
index.insert(r3)
let results = index.filterByDateRange(from: makeDate(daysFromNow: 0), to: makeDate(daysFromNow: 6))
XCTAssertEqual(results.count, 2)
XCTAssertTrue(results.contains(where: { $0.id == r1.id }))
XCTAssertTrue(results.contains(where: { $0.id == r2.id }))
}
func testMultiCriteriaSort() {
var index = ReservationIndex()
let baseDate = Date()
let r1 = Reservation(guestName: "Charlie", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 1), checkOut: baseDate.addingTimeInterval(86400 * 3))
let r2 = Reservation(guestName: "Alice", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 2), checkOut: baseDate.addingTimeInterval(86400 * 4))
let r3 = Reservation(guestName: "Bob", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 2), checkOut: baseDate.addingTimeInterval(86400 * 5))
let r4 = Reservation(guestName: "Diana", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 3), checkOut: baseDate.addingTimeInterval(86400 * 6))
let r5 = Reservation(guestName: "Eve", propertyName: "H", checkIn: baseDate.addingTimeInterval(86400 * 3), checkOut: baseDate.addingTimeInterval(86400 * 7))
index.insert(r1)
index.insert(r2)
index.insert(r3)
index.insert(r4)
index.insert(r5)
let sorted = index.sorted()
XCTAssertEqual(sorted[0].guestName, "Diana")
XCTAssertEqual(sorted[1].guestName, "Eve")
XCTAssertEqual(sorted[2].guestName, "Alice")
XCTAssertEqual(sorted[3].guestName, "Bob")
XCTAssertEqual(sorted[4].guestName, "Charlie")
}
}
You now have an indexed data structure that can answer the question "show me all confirmed reservations" in O(1) lookup time, plus O(k) to materialize the results. In the next step, you will tackle a different performance problem: preventing redundant API calls when the user types in a search bar.
Build the Debounced Search Controller
Your ReservationIndex gives you fast lookups on the server side. But on the client, there is a different performance problem: every keystroke in a search bar can fire a network request.
When a user types "John", you do not want 4 separate API calls for J, Jo, Joh, and John. Without debouncing you waste API quota and get flickering results. In this step you will build a search controller that uses Swift's cooperative Task cancellation to fire only the final query.
In this step, get ready to:
- Implement a DebouncedSearchController using async/await and Task cancellation.
- Write async unit tests proving only the final rapid query executes.
- Add interview-ready comments comparing this approach to Combine and DispatchWorkItem.
Create the DebouncedSearchController class
The debounce pattern works by cancelling the previous pending task every time new input arrives. Only after a quiet period (no new input) does the search actually execute.
- Create a new file Sources/ReservationEngine/DebouncedSearchController.swift and add the class shell with its properties and initializer:
import Foundation
@MainActor
public final class DebouncedSearchController {
private var currentTask: Task<Void, Never>?
private let debounceInterval: Duration
private let searchAction: @Sendable (String) async -> [Reservation]
public private(set) var results: [Reservation] = []
public private(set) var searchExecutionCount: Int = 0
public init(
debounceInterval: Duration = .milliseconds(300),
searchAction: @escaping @Sendable (String) async -> [Reservation]
) {
self.debounceInterval = debounceInterval
self.searchAction = searchAction
}
}
What does this code do?
The class is marked @MainActor because it drives UI updates. This ensures all access happens on the main thread.
- currentTask holds a reference to the in-flight debounce Task so we can cancel it.
- debounceInterval defaults to 300ms. The caller can customize this.
- searchAction is the injected closure that performs the actual search. It is @Sendable because it crosses the concurrency boundary.
- searchExecutionCount tracks how many times the search actually ran. This makes testing easy.
- Now add the search(query:) and cancel() methods inside the class, just above the closing }:
public func search(query: String) {
// Cancel any in-flight search task
currentTask?.cancel()
currentTask = Task { [debounceInterval, searchAction] in
do {
// Sleep for the debounce interval
try await Task.sleep(for: debounceInterval)
} catch {
return // Task was cancelled during sleep
}
// Belt-and-suspenders check after waking
guard !Task.isCancelled else { return }
let searchResults = await searchAction(query)
// Check again after the async search completes
guard !Task.isCancelled else { return }
self.results = searchResults
self.searchExecutionCount += 1
}
}
public func cancel() {
currentTask?.cancel()
currentTask = nil
}
How does the cancellation work?
Swift's Task cancellation is cooperative. The runtime does not kill the task. Instead, two mechanisms check for cancellation:
- Task.sleep(for:) throws CancellationError if the task is cancelled while sleeping. This is the primary cancellation path.
- Task.isCancelled is checked after waking and after the search completes. This handles the race where cancellation arrives between wakeup and execution.
- Save DebouncedSearchController.swift.
- Verify the file compiles by running this in Terminal:
swift build
You should see Build complete! with no errors.
Seeing a compile error?
- If you see an error about Duration not being found, confirm your Package.swift has platforms: [.macOS(.v13), .iOS(.v16)]. The Duration type requires macOS 13+.
- If you see an error about Reservation not being in scope, make sure your file is in the Sources/ReservationEngine/ directory alongside Models.swift.
- help me fix a compile error in DebouncedSearchController.swift
Write the debounced search tests
The key test is proving that rapid successive calls only trigger one actual search execution. You will use XCTest's async test methods to verify the debounce behavior.
- Create Tests/ReservationEngineTests/DebouncedSearchTests.swift with the first test that simulates rapid typing:
import XCTest
@testable import ReservationEngine
final class DebouncedSearchTests: XCTestCase {
@MainActor
func testRapidSearchOnlyExecutesFinalQuery() async throws {
var executedQueries: [String] = []
let controller = DebouncedSearchController(
debounceInterval: .milliseconds(100)
) { query in
executedQueries.append(query)
return [Reservation(guestName: query, propertyName: "Test", checkIn: Date(), checkOut: Date())]
}
// Simulate rapid typing: J, Jo, Joh, John
controller.search(query: "J")
controller.search(query: "Jo")
controller.search(query: "Joh")
controller.search(query: "John")
// Wait longer than debounce interval
try await Task.sleep(for: .milliseconds(250))
XCTAssertEqual(executedQueries.count, 1, "Only the final query should execute")
XCTAssertEqual(executedQueries.first, "John")
XCTAssertEqual(controller.searchExecutionCount, 1)
}
}
What does this test prove?
- Four rapid calls to search(query:) simulate a user typing "John" one character at a time.
- After waiting 250ms (longer than the 100ms debounce interval), only the final query "John" should have triggered the search closure.
- The @MainActor annotation on the test matches the controller's isolation. async throws lets XCTest run it asynchronously.
- Save the file and run the tests to confirm the core debounce behavior works:
swift test
You should see 10 tests passing, including the new testRapidSearchOnlyExecutesFinalQuery.
- Add the second test proving that explicit cancellation prevents execution. Place it inside the class, below the first test:
@MainActor
func testCancelPreventsExecution() async throws {
var executionCount = 0
let controller = DebouncedSearchController(
debounceInterval: .milliseconds(100)
) { _ in
executionCount += 1
return []
}
controller.search(query: "test")
controller.cancel()
try await Task.sleep(for: .milliseconds(250))
XCTAssertEqual(executionCount, 0, "Cancelled search should not execute")
}
What does this test prove?
Calling cancel() immediately after search(query:) cancels the underlying Task before it wakes from sleep. The assertion confirms the search closure never runs.
- Add the third test proving that searches separated by enough time both execute. Place it below testCancelPreventsExecution, still inside the class (before the final }):
@MainActor
func testSequentialSearchesWithDelayBothExecute() async throws {
var executedQueries: [String] = []
let controller = DebouncedSearchController(
debounceInterval: .milliseconds(50)
) { query in
executedQueries.append(query)
return []
}
controller.search(query: "first")
try await Task.sleep(for: .milliseconds(100))
controller.search(query: "second")
try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(executedQueries.count, 2)
XCTAssertEqual(executedQueries, ["first", "second"])
}
What does this test prove?
When two searches are separated by more than the debounce interval, both execute. This confirms the debounce does not permanently suppress searches. It only coalesces rapid bursts.
- Save DebouncedSearchTests.swift.
- Before you run the tests, predict: how many total tests should pass now across all test files?
- Run the full test suite:
swift test
You should see 12 tests passing. The 9 from earlier plus your 3 new debounced search tests.
Tests failing or timing out?
- If testRapidSearchOnlyExecutesFinalQuery fails with count 0, make sure the sleep duration (250ms) is longer than the debounce interval (100ms). The task needs time to wake and execute.
- If you see isolation errors, confirm every test method has the @MainActor annotation. The controller requires main actor access.
- help me debug my failing DebouncedSearchTests
Add interview-ready comments
An interviewer will want to hear why you chose Task cancellation over Combine or DispatchWorkItem. Adding a doc comment at the top of the file makes this reasoning immediately visible when walking through code.
- In DebouncedSearchController.swift, find this line at the top of the file:
@MainActor
public final class DebouncedSearchController {
- Add this doc comment block directly above @MainActor:
/// A debounced search controller that cancels in-flight searches when new input arrives.
///
/// Uses Swift's cooperative Task cancellation:
/// - Each new search call cancels the previous Task
/// - The Task sleeps for the debounce interval
/// - After waking, checks Task.isCancelled before executing
/// - Only the final query in a rapid burst actually triggers the search closure
///
/// Comparison to alternatives:
/// - Combine debounce(for:scheduler:): Requires Combine import, publisher chain setup.
/// Better for reactive pipelines but heavier dependency.
/// - DispatchWorkItem.cancel(): Pre-concurrency pattern. No structured lifetime management,
/// manual retain/release of work items, cancellation is not cooperative.
/// - Task cancellation (this approach): Zero dependencies beyond Swift concurrency,
/// cooperative cancellation is explicit, structured concurrency manages lifetime automatically.
Why document alternatives?
In an interview, showing you considered alternatives and can articulate trade-offs is stronger than just implementing the solution. This comment block gives you three talking points:
- Combine's debounce(for:scheduler:) is great for reactive pipelines but adds a framework dependency.
- DispatchWorkItem is the pre-concurrency pattern with no structured lifetime management.
- Task cancellation is zero-dependency, cooperative, and integrates with structured concurrency.
- Save DebouncedSearchController.swift.
- Run swift test one more time to confirm the comments did not break anything:
swift test
All 12 tests still pass.
✔️ Awesome, I've got everything!
Great. Double check you have saved both DebouncedSearchController.swift and DebouncedSearchTests.swift.
ⓧ I'd like to double check the full code
Scroll up and confirm you have each of these pieces in order:
- DebouncedSearchController.swift: the doc comment block comparing alternatives, followed by the @MainActor class with properties (currentTask, debounceInterval, searchAction, results, searchExecutionCount), the initializer, the search(query:) method, and the cancel() method.
- DebouncedSearchTests.swift: imports, the test class with testRapidSearchOnlyExecutesFinalQuery, testCancelPreventsExecution, and testSequentialSearchesWithDelayBothExecute.
Your debounced search controller is complete. You now have a modern Swift concurrency pattern that prevents redundant API calls without pulling in Combine or managing DispatchWorkItems manually. Next up, you will tackle the hardest concurrency challenge: actor isolation for thread-safe shared state.
Build the Actor-Based Booking State
Your debounced search controller proves you can manage Task lifecycle. Now you face a harder problem: multiple screens in a booking app read and write booking state simultaneously.
The old pattern is a class guarded by a DispatchQueue. Swift actors replace that with compile-time data race safety. The compiler itself prevents unsynchronized access, which is the strongest "modern Swift" signal you can give in an interview.
In this step, get ready to:
- Implement a BookingStateActor with state-validated check-in and check-out methods.
- Write async tests proving concurrent access is race-free.
- Discuss actor isolation, Sendable, and when to choose @MainActor vs a custom actor.
Create the error type and actor shell
The actor needs a custom error type to communicate invalid state transitions back to callers. You will define a BookingError enum first, then the actor's skeleton with its stored state and initializers.
- Create a new file BookingStateActor.swift inside Sources/ReservationEngine/ and add the following code:
import Foundation
public enum BookingError: Error, Equatable {
case reservationNotFound
case invalidStateTransition(current: ReservationStatus, attempted: ReservationStatus)
case alreadyInState(ReservationStatus)
}
public actor BookingStateActor {
private var reservations: [UUID: Reservation] = [:]
public init() {}
public init(reservations: [Reservation]) {
for reservation in reservations {
self.reservations[reservation.id] = reservation
}
}
public func addReservation(_ reservation: Reservation) {
reservations[reservation.id] = reservation
}
}
What does this code do?
BookingError conforms to both Error and Equatable so tests can assert on specific failure cases.
The actor keyword replaces class. It guarantees that only one task accesses reservations at a time. No locks, no queues, no runtime crashes from queue misuse.
- Below addReservation, add the query methods that let consumers read actor state. Paste this code above the actor's closing }:
public func reservation(for id: UUID) -> Reservation? {
reservations[id]
}
public func activeReservations() -> [Reservation] {
reservations.values.filter { $0.status == .checkedIn }
}
public func allReservations() -> [Reservation] {
Array(reservations.values)
}
What does this code do?
These methods are automatically isolated to the actor. Callers must await them, which is how Swift enforces safe cross-boundary access at compile time.
- Verify the actor compiles by running this in Terminal from your package root:
swift build
You should see Build complete! with no errors.
Seeing a compile error?
- Check that BookingStateActor.swift is inside Sources/ReservationEngine/ (not at the package root or inside Tests).
- Make sure the ReservationStatus type is available. It should already exist in Models.swift.
- Help me debug my BookingStateActor compile error.
Now you need the methods that validate and perform state transitions. These are where the actor's serialization guarantees pay off.
- Add the checkIn method below allReservations(), before the actor's closing }:
/// Transitions a reservation to .checkedIn. Throws if already checked in or not found.
public func checkIn(reservationID: UUID) throws {
guard var reservation = reservations[reservationID] else {
throw BookingError.reservationNotFound
}
guard reservation.status == .confirmed else {
if reservation.status == .checkedIn {
throw BookingError.alreadyInState(.checkedIn)
}
throw BookingError.invalidStateTransition(
current: reservation.status,
attempted: .checkedIn
)
}
reservation.status = .checkedIn
reservations[reservationID] = reservation
}
What does this code do?
The method validates that the reservation exists and is in .confirmed status before transitioning. If 10 tasks call checkIn simultaneously for the same ID, the actor serializes them. The first succeeds and the remaining 9 find .checkedIn status and throw.
- Add the checkOut method directly below checkIn:
/// Transitions a reservation to .checkedOut. Throws if not currently checked in.
public func checkOut(reservationID: UUID) throws {
guard var reservation = reservations[reservationID] else {
throw BookingError.reservationNotFound
}
guard reservation.status == .checkedIn else {
throw BookingError.invalidStateTransition(
current: reservation.status,
attempted: .checkedOut
)
}
reservation.status = .checkedOut
reservations[reservationID] = reservation
}
What does this code do?
checkOut mirrors checkIn but requires the reservation to already be in .checkedIn state. This enforces a valid state machine: confirmed → checkedIn → checkedOut.
- Save BookingStateActor.swift and confirm it compiles by running:
swift build
You should see Build complete! again with no errors.
✔️ Awesome, I've got everything!
Great. Confirm you have saved BookingStateActor.swift with the BookingError enum, the actor declaration, both initializers, the four query/mutation methods, checkIn, and checkOut.
ⓧ I'd like to double check the full code
Scroll up and confirm your file contains each of these sections in order:
- The BookingError enum with three cases (reservationNotFound, invalidStateTransition, alreadyInState).
- The actor declaration with private var reservations: [UUID: Reservation] and two initializers.
- addReservation, reservation(for:), activeReservations(), and allReservations() query methods.
- checkIn(reservationID:) with the double-guard validation.
- checkOut(reservationID:) with the .checkedIn guard.
Write concurrent tests that prove race safety
The real proof is in the tests. You will spawn many concurrent tasks that all hit the actor simultaneously and assert that no data is lost or duplicated.
- Create a new file BookingStateActorTests.swift inside Tests/ReservationEngineTests/ and add the first test:
import XCTest
@testable import ReservationEngine
final class BookingStateActorTests: XCTestCase {
func testConcurrentCheckInsAllSucceed() async {
let actor = BookingStateActor()
var ids: [UUID] = []
for i in 0..<100 {
let reservation = Reservation(
guestName: "Guest \(i)",
propertyName: "Hotel",
checkIn: Date(),
checkOut: Date().addingTimeInterval(86400),
status: .confirmed
)
ids.append(reservation.id)
await actor.addReservation(reservation)
}
await withTaskGroup(of: Void.self) { group in
for id in ids {
group.addTask {
try? await actor.checkIn(reservationID: id)
}
}
}
let active = await actor.activeReservations()
XCTAssertEqual(active.count, 100, "All 100 reservations should be checked in")
}
}
What does this test prove?
It spawns 100 concurrent tasks via withTaskGroup, each checking in a unique reservation. If the actor were a plain class, concurrent writes to the dictionary would cause data races. With actor isolation, every call is serialized and all 100 succeed.
- Run the tests to confirm this first test passes:
swift test
You should see 13 tests pass (your existing 12 plus the new one).
- Now add the contested check-in test. This one proves that when 10 tasks fight over the same reservation, exactly 1 wins. Add this method inside the test class, below testConcurrentCheckInsAllSucceed:
func testConcurrentCheckInSameReservationOnlyOneSucceeds() async {
let actor = BookingStateActor()
let reservation = Reservation(
guestName: "Contested Guest",
propertyName: "Hotel",
checkIn: Date(),
checkOut: Date().addingTimeInterval(86400),
status: .confirmed
)
await actor.addReservation(reservation)
var successCount = 0
var errorCount = 0
await withTaskGroup(of: Bool.self) { group in
for _ in 0..<10 {
group.addTask {
do {
try await actor.checkIn(reservationID: reservation.id)
return true
} catch {
return false
}
}
}
for await succeeded in group {
if succeeded {
successCount += 1
} else {
errorCount += 1
}
}
}
XCTAssertEqual(successCount, 1, "Only one concurrent check-in should succeed")
XCTAssertEqual(errorCount, 9, "Nine should fail with already-in-state error")
let final = await actor.reservation(for: reservation.id)
XCTAssertEqual(final?.status, .checkedIn)
}
Why is this the key interview test?
This is the "contested resource" pattern. Ten tasks race to mutate the same state. The actor serializes them, so the first task transitions the status to .checkedIn. The other 9 find it already checked in and throw alreadyInState. No locks, no crashes, no undefined behavior.
- Add the final test method that verifies invalid state transitions. Paste this below the contested test:
func testCheckOutRequiresCheckedIn() async throws {
let actor = BookingStateActor()
let reservation = Reservation(
guestName: "Test",
propertyName: "Hotel",
checkIn: Date(),
checkOut: Date().addingTimeInterval(86400),
status: .confirmed
)
await actor.addReservation(reservation)
do {
try await actor.checkOut(reservationID: reservation.id)
XCTFail("Should have thrown")
} catch let error as BookingError {
XCTAssertEqual(error, .invalidStateTransition(current: .confirmed, attempted: .checkedOut))
}
}
What does this test do?
It attempts to check out a reservation that is still in .confirmed status. The actor correctly rejects this as an invalid transition, proving the state machine logic works independently of concurrency.
- Save BookingStateActorTests.swift.
Before you run the tests, predict: will all 15 tests pass, or will any fail?
- Run the full test suite:
swift test
You should see all 15 tests pass. The concurrent tests run the actor through 110 simultaneous tasks and every assertion holds.
Tests failing?
- If testConcurrentCheckInSameReservationOnlyOneSucceeds reports more than 1 success, confirm your checkIn method checks for .checkedIn status and throws alreadyInState.
- If you see reservationNotFound errors, make sure addReservation is called with await in the test setup.
- Help me debug my BookingStateActorTests.
✔️ Awesome, I've got everything!
Great. Confirm you have saved BookingStateActorTests.swift with all three test methods.
ⓧ I'd like to double check the full code
Scroll up and confirm your test file contains each of these methods in order:
- testConcurrentCheckInsAllSucceed spawning 100 tasks with unique reservations.
- testConcurrentCheckInSameReservationOnlyOneSucceeds spawning 10 tasks for one reservation, asserting 1 success and 9 errors.
- testCheckOutRequiresCheckedIn asserting the invalidStateTransition error.
Prepare your interview talking points
When an interviewer asks about your concurrency approach, you need more than working code. You need to articulate why actors are the right choice and what alternatives you considered.
Why actor over class + DispatchQueue?
A class with a serial DispatchQueue gives runtime safety, but the compiler cannot detect mistakes. You can forget to dispatch, call sync on your own queue (deadlock), or pass a non-thread-safe reference type across the boundary. Actors move these errors to compile time.
Sendable conformance on Reservation ensures safe cross-boundary transfer. The struct is a value type with only Sendable-conforming stored properties, so passing it in and out of the actor copies it safely.
💡 When to use @MainActor vs a custom actor?
Use @MainActor for UI state that drives view updates (ViewModels, ObservableObject properties). Use a custom actor like BookingStateActor for background processing and shared business state accessed from multiple contexts.
To expose actor state to SwiftUI, use a @MainActor-annotated bridging method that returns Sendable snapshots for the View to consume. Your DebouncedSearchController from the previous step is an example of the @MainActor pattern in action.
Secret mission
Cursor-Based Pagination with TaskGroup
Real booking apps load reservations in pages. Can you combine async sequences, TaskGroup, and actor state management into a single cohesive pagination engine with concurrent prefetching?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources on disk, compress them for backup, or delete them entirely.
Resources you used:
- The ReservationEngine Swift package directory (source files, tests, build artifacts).
Keep everything running
No action needed. Choose this if you want to keep the package available for interview review, further iteration, or adding more data structure implementations.
- The ReservationEngine package stays in your Xcode workspace and on disk wherever you created it.
- You can reopen it at any time by double-clicking Package.swift in Finder.
Pause - I'll come back to this later
Compress the package folder to free up disk space while preserving your work for later.
- In Finder, right-click the ReservationEngine folder and select Compress.
- This creates a .zip archive you can store or move to an external drive.
- Optionally delete the original folder after confirming the zip file is intact.
Delete - I don't want to use this again
Remove all project files from your machine permanently.
- Open Terminal and navigate to the parent directory where your ReservationEngine folder lives.
- Remove the package directory by running:
rm -rf ReservationEngine
- Confirm the folder is gone by running:
ls | grep ReservationEngine
You should see no output, confirming the directory has been deleted.
Nice Work!
Nice Work!
Nice work! You just built a production-grade Swift package demonstrating four distinct data structure and concurrency patterns, each backed by unit tests you can walk through in any system design interview.
You've learned how to:
- Build a generic LRU Cache from scratch using a Dictionary + Doubly Linked List for O(1) get, put, and eviction operations, with tests proving deterministic eviction behavior.
- Create a debounced search controller using async/await and cooperative Task cancellation, replacing older Combine and DispatchWorkItem patterns with structured concurrency.
- Design an actor-isolated BookingState with compile-time data race safety, proving through concurrent tests that only one of 10 simultaneous check-in attempts succeeds while the actor serializes access.
- Secret Mission: Built a cursor-based PaginationEngine actor with TaskGroup prefetching and cooperative cancellation for concurrent page loading.
Ready to quiz yourself?