iosNudge

The 45-Minute Framework

Chapter 2 of 8 5 min read

The plan at a glance

Use the same six steps for every prompt. Then you never wonder "what now?".

StepTimeWhat you produce
1. Requirements5 minA short list: features, non-functional needs, out of scope
2. High-level architecture8 minA box-and-arrow diagram
3. Data model5 min2 to 4 Swift types
4. API and interfaces5 minServer endpoints and the key Swift protocols
5. Deep dives17 min2 or 3 topics in depth
6. Wrap-up5 minTrade-offs, risks, what you would do next

Is the round 60 minutes? Give the extra time to the deep dives.

Say the plan out loud at the start:

"I'd like to start with requirements, then draw the main parts, then the data model and API. After that I'd go deep on two or three areas. Does that work for you?"

This shows structure. It also lets the interviewer steer you early.

This chapter uses one example the whole way: "Design a photo feed app."

Step 1: Requirements (5 minutes)

Goal: agree on what you will build. Ask a few questions, then write the answers down.

Functional
- Scroll a feed of posts (image, author, caption, like count)
- Like and unlike a post
- Pull to refresh, load more at the bottom

Non-functional
- Opens fast, shows saved posts when offline
- Smooth scrolling on older iPhones
- Works on slow networks
- VoiceOver and Dynamic Type support

Out of scope
- Uploading photos, comments, stories

Keep 3 or 4 core features. Move the rest to "out of scope". You cannot design everything in 45 minutes, and the interviewer knows it. The next chapter covers which questions to ask.

Step 2: High-level architecture (8 minutes)

Draw the main parts. Start at the screen and go down to the network.

+-------------+     +----------------+     +------------------+
|  FeedView   | --> | FeedViewModel  | --> |  FeedRepository  |
+-------------+     +----------------+     +------------------+
                                              |            |
                                              v            v
                                        +-----------+  +-----------+
                                        |  FeedAPI  |  | FeedStore |
                                        | (network) |  |  (disk)   |
                                        +-----------+  +-----------+
                                              |
                                              v
                                          [ Server ]

ImageLoader (memory + disk cache) is shared by all feed cells.

Give each box one sentence:

  • FeedView: shows posts, sends taps to the view model.
  • FeedViewModel: holds screen state (posts, loading, error).
  • FeedRepository: the single source of truth for posts. It reads the disk and the network.
  • FeedAPI: talks to the server.
  • FeedStore: saves posts on disk for offline use.

Then explain the data flow in two sentences: "The view model asks the repository for posts. The repository returns saved posts at once, then fresh posts from the API."

Step 3: Data model (5 minutes)

Write the core types. Keep them small. Add only fields the features need.

import Foundation

struct User: Hashable, Codable, Sendable {
    let id: String
    let name: String
    let avatarURL: URL?
}

struct Post: Identifiable, Hashable, Codable, Sendable {
    let id: String
    let author: User
    let imageURL: URL
    let caption: String
    var likeCount: Int
    var isLiked: Bool
    let createdAt: Date
}

struct FeedPage: Sendable {
    let posts: [Post]
    let nextCursor: String?   // nil means "no more pages"
}

Say a few words about each choice. "IDs are strings from the server. likeCount and isLiked are var because the user can change them. Codable lets us save posts to disk."

Step 4: API and interfaces (5 minutes)

Two parts: the server endpoints and the Swift protocols.

GET    /v1/feed?cursor={cursor}&limit=20   -> 200 { "posts": [...], "next_cursor": "..." }
PUT    /v1/posts/{id}/like                 -> 204
DELETE /v1/posts/{id}/like                 -> 204
protocol FeedRepository: Sendable {
    func cachedFeed() async -> [Post]
    func fetchFeed(cursor: String?) async throws -> FeedPage
    func setLiked(_ liked: Bool, postID: String) async throws
}

Why PUT and DELETE and not one "toggle" call? They are idempotent. Idempotent means you can send the same request twice and get the same result. So a retry is always safe.

Step 5: Deep dives (17 minutes)

This is where you show depth. Pick 2 or 3 topics that matter for this product. Then ask:

"I'd like to go deeper on image loading and the offline cache. Is there an area you prefer?"

Good deep dives by product:

ProductGood deep dives
Photo feedImage loading, pagination, prefetching, offline cache
ChatReal-time updates, offline send queue, message order
NotesOffline sync, conflict resolution
Ride trackingLocation updates, battery, background modes
VideoStreaming, memory, downloads for offline

Use the same shape for each topic: problem, options, choice, trade-off.

Example: "Liking a post should feel instant." The usual answer is an optimistic update. You change the UI first, call the server, and roll back if the call fails.

import Observation

@MainActor
@Observable
final class FeedViewModel {
    private(set) var posts: [Post] = []
    var errorMessage: String?
    private let repository: FeedRepository

    init(repository: FeedRepository) {
        self.repository = repository
    }

    func toggleLike(postID: String) async {
        guard let post = posts.first(where: { $0.id == postID }) else { return }
        let liked = !post.isLiked
        apply(liked: liked, to: postID)                   // 1. update the UI now
        do {
            try await repository.setLiked(liked, postID: postID)   // 2. tell the server
        } catch {
            apply(liked: !liked, to: postID)              // 3. roll back on failure
            errorMessage = "Could not update the like. Try again."
        }
    }

    private func apply(liked: Bool, to postID: String) {
        // Look up the index again: the array can change while we wait for the server.
        guard let index = posts.firstIndex(where: { $0.id == postID }),
              posts[index].isLiked != liked else { return }
        posts[index].isLiked = liked
        posts[index].likeCount += liked ? 1 : -1
    }
}

Point out the small detail in apply. After an await, the list may have changed, for example after a refresh. Never keep an old index across an await. Interviewers love this detail.

Step 6: Wrap-up (5 minutes)

Summarize in 30 seconds. Then name trade-offs and next steps.

"To sum up: MVVM with a repository that serves cached posts first, then fresh ones. Cursor pagination. Images are downsampled and cached in memory and on disk. Likes are optimistic. The main trade-off is that offline users may see old like counts. With more time I'd add metrics for scroll hitches and image load time, and ship the new feed behind a feature flag."

Good topics for the wrap-up:

  • the 2 or 3 biggest trade-offs
  • what can go wrong (risks)
  • how you would test it
  • how you would measure success (metrics)
  • how you would roll it out

Managing time

  • Check the clock. By minute 15 the diagram should be done. By minute 25 the API should be done.
  • Follow the interviewer. If they dig into something early, go with it. Their interest is a signal.
  • Never go silent. If you need to think, say "Let me think about this for a moment."
  • Running late? Say it: "I'll skip the details of X so we have time for Y."

Phrases that help

  • "Let me write down the requirements so we agree on the scope."
  • "I'll start with the simple version and then improve it."
  • "There are two options. A gives us X, B gives us Y. For this app I pick A, because..."
  • "This is the part I'd test most carefully."
  • "Is this the level of detail you want, or should I go deeper?"

Practice tip

Take one prompt and run the six steps with a timer. Record yourself. Listen for long silences and for places where you said "it depends" without saying on what. Fix those first.