Non-Functional Requirements in Mobile System Design

By Ishan Khanna LinkedInUpdated May 25, 2026
mobile-system-designinterviewsnon-functional-requirementsperformancescalability

I once watched a candidate design an entire chat application — message sending, group chats, read receipts, the works — without once mentioning what happens when the user walks into a subway tunnel. Forty-five minutes of functional features, zero seconds on the constraints those features operate under. They didn't get the offer.

That's the gap non-functional requirements fill. Features describe what the app does. Non-functional requirements describe how well it does it — under real-world conditions, on real devices, with real networks.

If you can only list features, you're thinking like a junior engineer. If you can articulate the constraints those features must satisfy, you're thinking like someone who has shipped software to millions of users. Interviewers at companies like Google, Meta, and Airbnb are looking for the second person.

What Non-Functional Requirements Are (and Why Interviewers Care)#

Non-functional requirements (NFRs) are the quality attributes your system must meet. They don't describe a feature — they describe a standard that feature must hit.

Here's the difference in practice. Two candidates are both designing a social media feed:

Candidate A: "The app should load the feed quickly."

Candidate B: "Feed loading should complete within 300ms on a 4G connection and under 1.5 seconds on 3G. We should show a skeleton UI within 100ms of the user tapping into the feed, then progressively load images. On first launch, we target a cold start time under 2 seconds on mid-range devices."

Candidate B gets the offer. Not because they memorized numbers, but because those numbers reveal they've thought about the problem. They understand what "fast" actually means on a phone sitting in someone's pocket on a crowded train.

Interviewers care about NFRs for three reasons:

  1. They expose depth of experience. Anyone can read an API doc and list features. NFRs come from having shipped apps and watched them fail in production.
  2. They drive architectural decisions. An offline-first requirement completely changes your data layer. A strict battery budget kills your real-time polling design. NFRs are where architecture gets interesting.
  3. They separate "works in a demo" from "works at scale." Every app works on fast WiFi with a fresh install. NFRs force you to think about the other 99% of situations.

Performance Requirements for Mobile#

Performance on mobile is not the same as performance on web. A web app runs on a machine with 16GB of RAM and a fiber connection. A mobile app runs on a device with 3-6GB of RAM shared across dozens of apps, connected to a cell tower that might be overloaded.

Latency Targets#

Here are realistic numbers you should know:

MetricGoodAcceptablePoor
Cold start time< 1s1-2s> 3s
Warm start time< 500ms500ms-1s> 1.5s
API response rendering< 300ms300ms-1s> 2s
Screen transition< 150ms150-300ms> 500ms
Touch response< 100ms100-200ms> 300ms

These aren't arbitrary. Google's research shows that 53% of mobile users abandon a page that takes longer than 3 seconds to load. Apple's Human Interface Guidelines recommend responding to touch within 100ms — anything longer feels laggy to the user.

Frame Rate#

Smooth scrolling means 60 frames per second. That gives you 16.67ms per frame to do everything — layout, drawing, any computation tied to the scroll. Drop below that and you get visible jank.

On high-refresh-rate devices (120Hz on modern iPhones and flagships), the budget drops to 8.33ms per frame. In an interview, mention this difference — it shows you're keeping up with hardware evolution.

// Android: Detecting frame drops with Choreographer
class FrameMonitor {
    private var lastTimestampNanos: Long = 0

    private val frameCallback = object : Choreographer.FrameCallback {
        override fun doFrame(frameTimeNanos: Long) {
            if (lastTimestampNanos > 0) {
                val frameDuration = (frameTimeNanos - lastTimestampNanos) / 1_000_000_000.0
                val fps = 1.0 / frameDuration
                if (fps < 55) {
                    // Log frame drop — this is where jank becomes visible
                    Log.d("FrameMonitor", "Frame drop detected: ${fps.toInt()} FPS")
                }
            }
            lastTimestampNanos = frameTimeNanos
            Choreographer.getInstance().postFrameCallback(this)
        }
    }

    fun startMonitoring() {
        Choreographer.getInstance().postFrameCallback(frameCallback)
    }
}
// iOS: Detecting frame drops with CADisplayLink
class FrameMonitor {
    private var displayLink: CADisplayLink?
    private var lastTimestamp: CFTimeInterval = 0
    
    func startMonitoring() {
        displayLink = CADisplayLink(target: self, selector: #selector(onFrame))
        displayLink?.add(to: .main, forMode: .common)
    }
    
    @objc private func onFrame(_ link: CADisplayLink) {
        if lastTimestamp > 0 {
            let frameDuration = link.timestamp - lastTimestamp
            let fps = 1.0 / frameDuration
            if fps < 55 {
                // Log frame drop — this is where jank becomes visible
                print("Frame drop detected: \(Int(fps)) FPS")
            }
        }
        lastTimestamp = link.timestamp
    }
}

Time to Interactive#

Cold start time gets all the attention, but time to interactive (TTI) is what actually matters. Your app can render a screen in 800ms, but if the user can't tap anything for another 2 seconds because you're doing a sync in the main thread, you've failed.

In an interview, distinguish between these: "We render the feed skeleton in 200ms, load cached data by 500ms, and the feed is fully interactive with fresh data by 1.2 seconds."

Interview Tip: When discussing performance, always specify the device tier and network condition. "300ms on a Pixel 8 Pro on WiFi" is a very different claim from "300ms on a Galaxy A14 on 3G." Interviewers want to know you think about the low end, not just the flagship.

Scalability on the Client Side#

When candidates hear "scalability," they immediately think about servers handling millions of requests. But client-side scalability is a real concern, and it comes up in mobile design interviews constantly.

Large Data Sets in Memory#

Think about a chat app. A conversation might have 50,000 messages. You cannot load all of them into memory. This seems obvious, but I've seen candidates design message storage that fetches an entire conversation history into a list.

The solution is pagination — but the kind of pagination matters:

// Android: Paging with Room for a chat application
@Dao
interface MessageDao {
    @Query("""
        SELECT * FROM messages 
        WHERE conversation_id = :conversationId 
        ORDER BY created_at DESC 
        LIMIT :limit OFFSET :offset
    """)
    suspend fun getMessages(
        conversationId: String,
        limit: Int = 30,
        offset: Int = 0
    ): List<Message>

    // Cursor-based pagination — better for real-time data
    @Query("""
        SELECT * FROM messages 
        WHERE conversation_id = :conversationId 
        AND created_at < :beforeTimestamp 
        ORDER BY created_at DESC 
        LIMIT :limit
    """)
    suspend fun getMessagesBefore(
        conversationId: String,
        beforeTimestamp: Long,
        limit: Int = 30
    ): List<Message>
}
// iOS: Paging with Core Data for a chat application
class MessageStore {
    private let context: NSManagedObjectContext

    init(context: NSManagedObjectContext) {
        self.context = context
    }

    func getMessages(
        conversationId: String,
        limit: Int = 30,
        offset: Int = 0
    ) throws -> [Message] {
        let request = Message.fetchRequest()
        request.predicate = NSPredicate(format: "conversationId == %@", conversationId)
        request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
        request.fetchLimit = limit
        request.fetchOffset = offset
        return try context.fetch(request)
    }

    // Cursor-based pagination — better for real-time data
    func getMessagesBefore(
        conversationId: String,
        beforeTimestamp: Date,
        limit: Int = 30
    ) throws -> [Message] {
        let request = Message.fetchRequest()
        request.predicate = NSPredicate(
            format: "conversationId == %@ AND createdAt < %@",
            conversationId, beforeTimestamp as NSDate
        )
        request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
        request.fetchLimit = limit
        return try context.fetch(request)
    }
}

Offset-based pagination breaks when new messages arrive — the offsets shift and you either skip messages or show duplicates. Cursor-based pagination (using a timestamp or message ID as the anchor) handles real-time inserts correctly. Mention this trade-off in an interview and you'll stand out.

View Recycling#

RecyclerView on Android and UICollectionView with diffable data sources on iOS exist specifically because you can't render 10,000 cells at once. In a design interview, mention that your list implementation reuses views and only keeps a screen's worth of cells in memory at any time.

Data Growth Over Time#

A photo-sharing app accumulates cached data over weeks and months. Without a cache eviction strategy, your app's disk usage grows unbounded until the OS starts warning the user — or worse, kills your app. In your design, specify an LRU eviction policy and a maximum cache size (say, 500MB for images, with older entries evicted first).

Reliability and Availability#

Mobile apps face reliability challenges that web apps mostly don't. The network drops. The OS kills your process. The user force-quits mid-operation.

Network Failure Handling#

Every network call can fail. The question is how your app responds. There are three strategies:

  1. Retry with exponential backoff. Good for transient failures. Start at 1 second, double each time, cap at 30 seconds. Add jitter so a million clients don't all retry at exactly the same moment.
  2. Queue and sync later. Good for write operations. The user creates a post offline — queue it locally, sync when connectivity returns.
  3. Degrade gracefully. Show cached data with a banner: "You're offline. Showing saved content." The app stays useful even without a network.
// Android: Retry with exponential backoff and jitter
suspend fun <T> fetchWithRetry(
    maxAttempts: Int = 3,
    baseDelayMillis: Long = 1_000L,
    operation: suspend () -> T
): T {
    var lastError: Throwable? = null

    repeat(maxAttempts) { attempt ->
        try {
            return operation()
        } catch (error: Throwable) {
            lastError = error
            if (attempt < maxAttempts - 1) {
                val backoff = baseDelayMillis * 2.0.pow(attempt)
                val jitter = Random.nextDouble(0.0, backoff * 0.1)
                delay((backoff + jitter).toLong())
            }
        }
    }
    throw lastError!!
}
// iOS: Retry with exponential backoff and jitter
func fetchWithRetry<T>(
    maxAttempts: Int = 3,
    baseDelay: TimeInterval = 1.0,
    operation: @escaping () async throws -> T
) async throws -> T {
    var lastError: Error?
    
    for attempt in 0..<maxAttempts {
        do {
            return try await operation()
        } catch {
            lastError = error
            if attempt < maxAttempts - 1 {
                let delay = baseDelay * pow(2.0, Double(attempt))
                let jitter = Double.random(in: 0...(delay * 0.1))
                try await Task.sleep(nanoseconds: UInt64((delay + jitter) * 1_000_000_000))
            }
        }
    }
    throw lastError!
}

Crash-Free Rate#

Production apps aim for a crash-free rate of 99.5% or higher. Google Play Console flags apps below 98.5%. Apple has similar thresholds for App Store featuring. In your design, mention crash reporting (Firebase Crashlytics or Sentry) and what your target crash-free rate is.

A subtle point: crash-free rate is measured per session, not per user. A single user who crashes 10 times drags the metric down significantly. Design your error handling to catch and recover from exceptions in non-critical paths rather than letting them propagate and crash the app.

Handling Process Death#

On Android, the OS can kill your process at any time when the app is in the background. On iOS, the same happens, though the lifecycle is slightly different. Your app needs to save and restore state so the user doesn't lose work.

This means if someone is composing a long message and switches to another app, when they return, that draft should still be there. If they were on step 3 of a 5-step checkout flow, they should return to step 3.

Offline Support#

This is the NFR that changes everything. The moment you say "this app needs to work offline," your entire data architecture shifts.

Offline-First vs. Online-First#

Not every app needs to work offline. But you need to decide — and articulate why.

ConsiderationOffline-FirstOnline-First
Data storageFull local database (Room, Core Data)Minimal cache, fetch on demand
Sync complexityHigh — conflict resolution neededLow — server is source of truth
Initial development cost2-3x more effortStandard effort
User experience on poor networksSeamlessDegraded or broken
Data consistencyEventual consistencyStrong consistency
Best forMaps, notes, messaging, emailLive streaming, stock trading, social feeds

A maps app must work offline. Someone driving through a dead zone cannot wait for a network connection to see their next turn. Google Maps downloads map tiles for offline regions — this is a hard requirement.

A live streaming app cannot work offline. There is no offline version of a live video. The best you can do is show a "reconnecting" state and resume when the network returns.

Most apps fall somewhere in between. An e-commerce app should let users browse cached product listings offline but obviously can't process a payment without a network.

Conflict Resolution#

The moment two devices (or a device and a server) can modify the same data independently, you need a conflict resolution strategy. The three common approaches:

  1. Last write wins. Simple but lossy. The most recent timestamp overwrites everything. Fine for profile updates, bad for collaborative editing.
  2. Server wins. The client always defers to the server's version. Simple to implement but can discard valid user changes.
  3. Merge. Attempt to combine both changes. This is what apps like Notion and Google Docs do, and it's significantly harder to implement correctly.

In an interview, pick the strategy that matches the use case and explain why. Don't just say "we'll handle conflicts." Say how.

Battery and Resource Efficiency#

A user will uninstall your app before they'll tolerate it draining their battery. Battery efficiency isn't a nice-to-have — it's a retention requirement.

The Real Cost of Background Work#

Here's something most candidates don't know with specifics: continuous GPS polling at high accuracy drains roughly 10-15% of battery per hour on a typical smartphone. Drop the polling interval from every second to every 30 seconds, and that drops to about 2-3% per hour. Use significant location change monitoring (which relies on cell tower changes rather than GPS), and you're under 1%.

// Android: Battery-efficient location strategies
class LocationTracker(private val context: Context) {
    
    // High accuracy — use only when app is in foreground and user is actively navigating
    fun startActiveNavigation(): LocationRequest {
        return LocationRequest.Builder(
            Priority.PRIORITY_HIGH_ACCURACY,
            1000L // 1 second interval
        ).setMinUpdateDistanceMeters(5f)
         .build()
    }
    
    // Balanced — background tracking for ride-sharing or delivery apps
    fun startBackgroundTracking(): LocationRequest {
        return LocationRequest.Builder(
            Priority.PRIORITY_BALANCED_POWER_ACCURACY,
            30_000L // 30 second interval
        ).setMinUpdateDistanceMeters(50f)
         .build()
    }
    
    // Passive — minimal battery impact, piggybacks on other apps' requests
    fun startPassiveTracking(): LocationRequest {
        return LocationRequest.Builder(
            Priority.PRIORITY_PASSIVE,
            60_000L // 1 minute interval
        ).build()
    }
}
// iOS: Battery-efficient location strategies
class LocationTracker {
    private let locationManager = CLLocationManager()

    // High accuracy — use only when app is in foreground and user is actively navigating
    func startActiveNavigation() {
        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        locationManager.distanceFilter = 5 // meters
        locationManager.startUpdatingLocation()
    }

    // Balanced — background tracking for ride-sharing or delivery apps
    func startBackgroundTracking() {
        locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        locationManager.distanceFilter = 50 // meters
        locationManager.allowsBackgroundLocationUpdates = true
        locationManager.pausesLocationUpdatesAutomatically = true
        locationManager.startUpdatingLocation()
    }

    // Passive — minimal battery impact, relies on cell tower changes instead of GPS
    func startPassiveTracking() {
        locationManager.startMonitoringSignificantLocationChanges()
    }
}

Wake Locks and Background Processing#

On Android, a wake lock keeps the CPU running when the screen is off. This is sometimes necessary — a music player needs to keep playing — but misusing wake locks is one of the fastest ways to drain a battery.

On iOS, background execution is much more restricted. You get roughly 30 seconds of background time after the user leaves your app. Beyond that, you need a specific background mode (audio, location, VoIP) or you use BGTaskScheduler for deferred work. The OS controls when your scheduled tasks actually run.

Design your background work around these constraints:

  • Batch network requests. Don't sync every record individually. Bundle changes and send them in a single request.
  • Respect Doze mode (Android) and Low Power Mode (iOS). When the device is in power-saving mode, reduce or defer non-critical work.
  • Use push notifications instead of polling. A push notification costs almost no battery. Polling every 30 seconds costs a meaningful amount over a day.

Interview Tip: When designing any feature that involves background work, explicitly mention the battery trade-offs. Say something like: "We could poll for new messages every 10 seconds, but that would keep the radio active constantly. Instead, we'll use push notifications for new message alerts and only open a WebSocket connection while the app is in the foreground." This shows you think about the device as a whole, not just your app.

Security and Privacy#

Security in mobile system design interviews falls into two categories: things interviewers expect you to mention, and things that genuinely impress them.

What Interviewers Expect#

HTTPS everywhere. This should be a given. All API communication over TLS 1.2 or higher. Both iOS (App Transport Security) and Android (Network Security Config) enforce this by default in modern versions.

Authentication tokens stored securely. On iOS, use the Keychain. On Android, use the EncryptedSharedPreferences or the Keystore system. Never store tokens in plain SharedPreferences or UserDefaults.

// Android: Storing auth tokens in EncryptedSharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

fun storeToken(context: Context, token: String, key: String): Boolean {
    return try {
        // Master key lives in the hardware-backed Android Keystore
        val masterKey = MasterKey.Builder(context)
            .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
            .build()

        val prefs = EncryptedSharedPreferences.create(
            context,
            "secure_auth_prefs",
            masterKey,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )

        prefs.edit().putString(key, token).commit()
    } catch (e: Exception) {
        false
    }
}
// iOS: Storing auth tokens in Keychain
import Security

func storeToken(_ token: String, forKey key: String) -> Bool {
    let data = token.data(using: .utf8)!
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    ]
    
    // Delete any existing item first
    SecItemDelete(query as CFDictionary)
    
    let status = SecItemAdd(query as CFDictionary, nil)
    return status == errSecSuccess
}

Data at rest encryption. If you're storing sensitive data locally (messages, health data, financial records), encrypt the database. SQLCipher for SQLite, or use the built-in encrypted storage options each platform provides.

What Impresses Interviewers#

Certificate pinning. Instead of trusting any certificate signed by a trusted CA, your app pins the expected server certificate (or its public key). This prevents man-in-the-middle attacks even if a CA is compromised. Mention the operational cost though — you need a rotation strategy, or a pinned certificate expiring will brick your app's networking.

Biometric authentication for sensitive operations. Using Face ID or fingerprint auth before showing financial data or approving transactions. The key detail: biometric data never leaves the device's secure enclave. Your app never sees the fingerprint — it gets a yes/no from the OS.

Privacy by design. Minimizing data collection, anonymizing analytics, supporting GDPR's right to deletion. If you're designing a location-based app, mention that you store location data only as long as needed and allow users to delete their history. At Meta, I noticed interviewers particularly valued candidates who brought up privacy considerations without being prompted.

Accessibility#

Accessibility is increasingly asked about in interviews, and candidates who address it unprompted signal maturity. It's also the law in many jurisdictions — apps that don't meet WCAG 2.1 AA standards can face legal consequences.

Screen Reader Support#

VoiceOver (iOS) and TalkBack (Android) need meaningful labels on every interactive element. A button that just says "button" is useless to a blind user. A button that says "Send message to Sarah" is useful.

// Android: Proper content descriptions for accessibility
@Composable
fun LikeButton(isLiked: Boolean, postAuthor: String, onToggle: () -> Unit) {
    IconButton(
        onClick = onToggle,
        modifier = Modifier.semantics {
            contentDescription = if (isLiked) {
                "Unlike $postAuthor's post"
            } else {
                "Like $postAuthor's post"
            }
            stateDescription = if (isLiked) "Liked" else "Not liked"
        }
    ) {
        Icon(
            imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
            contentDescription = null // Handled by parent semantics
        )
    }
}
// iOS: Proper accessibility labels for VoiceOver
struct LikeButton: View {
    let isLiked: Bool
    let postAuthor: String
    let onToggle: () -> Void

    var body: some View {
        Button(action: onToggle) {
            Image(systemName: isLiked ? "heart.fill" : "heart")
        }
        .accessibilityLabel(
            isLiked ? "Unlike \(postAuthor)'s post" : "Like \(postAuthor)'s post"
        )
        .accessibilityValue(isLiked ? "Liked" : "Not liked")
    }
}

Dynamic Type and Text Scaling#

Users can increase their system font size up to 300% on both platforms. Your app must not break when this happens. Text should reflow, not get clipped. Layouts should expand vertically, not overflow horizontally.

In your design, mention that you use relative font sizes (sp on Android, Dynamic Type on iOS) rather than fixed pixel values. This is a small detail that signals you've built production apps.

Color Contrast#

WCAG 2.1 AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. If your design uses light gray text on a white background (which looks sleek in Figma), it fails accessibility standards.

Mention that your app supports both light and dark modes, and that you've verified contrast ratios in both. If you want to go further, mention supporting the system-level "increase contrast" accessibility setting.

Presenting NFRs in an Interview#

Here's where all of this comes together. The worst thing you can do is dump a laundry list of NFRs at the start of the interview: "OK so we need performance, scalability, reliability, offline support, security, accessibility, and battery efficiency." That's reciting a textbook, not designing a system.

Instead, weave NFRs into your design as you go. Let me show you with an example.

The Wrong Way#

"Let me start by listing the non-functional requirements. We need low latency, offline support, security, scalability, and battery efficiency. OK, now let me design the feed feature..."

This treats NFRs as a checkbox exercise. The interviewer hears you listing terms without understanding how they shape the design.

The Right Way#

"For the Instagram-style feed, the first question is: does this need to work offline? I think yes — users should be able to scroll through previously loaded posts even without a network. That means we need a local database to cache feed items. I'd use Room on Android and Core Data on iOS, with an LRU eviction policy capped at 500 posts.

Now, because we're caching locally, feed loading on repeat visits should be fast — we can show cached content within 200ms while fetching fresh data in the background. On a 3G connection, fresh data might take 2-3 seconds, so the user sees stale-but-fast content immediately and fresh content as it arrives. This progressive loading pattern means we need a diffing mechanism — DiffUtil on Android or diffable data sources on iOS — so we can insert new posts without the feed jumping.

For the images, we're looking at the main battery and bandwidth concern. We'll serve WebP images at 3 different resolutions — thumbnail for the feed (200px), medium for expanded view (600px), and full resolution only on WiFi when the user explicitly zooms in. This keeps the cellular data cost down and avoids loading 4MB images on a connection that can barely handle 500KB."

See the difference? Every NFR emerges naturally from a design decision. Offline support leads to a caching strategy. The caching strategy enables fast loading. Image optimization addresses both performance and battery. The interviewer sees you thinking, not reciting.

A Framework for Weaving In NFRs#

As you design each major component, ask yourself three questions:

  1. What happens when this fails? (reliability, offline)
  2. What happens when this scales? (memory, pagination, cache size)
  3. What does this cost the device? (battery, bandwidth, storage)

Answer those three questions for every component, and you'll naturally cover the NFRs that matter without ever dumping a list.

Putting It All Together#

Here's a quick-reference checklist you can use when practicing. For any mobile system design question, make sure you've addressed the NFRs that matter most for that specific problem. Not all of them apply to every design — picking the right ones to emphasize is part of the skill.

NFR CategoryKey Question to AskExample Metric
PerformanceHow fast must the core action complete?Feed loads in < 300ms from cache
ScalabilityWhat happens with large data sets?Paginate at 30 items, recycle views
ReliabilityWhat happens when the network drops?Show cached data, queue writes
Offline supportMust this feature work without internet?Cache last 500 feed items locally
BatteryDoes this feature run in the background?Push notifications, not polling
SecurityWhat data needs protection?Tokens in Keychain, encrypted DB
AccessibilityCan all users interact with this?VoiceOver labels, Dynamic Type

The candidate who got rejected at the start of this article? They knew how to design a chat app's features. They could draw boxes and arrows on a whiteboard. What they couldn't do was explain what happens when those boxes and arrows meet the real world — patchy networks, limited batteries, diverse devices, and users who need accessibility features.

Non-functional requirements are where you prove you've been in that real world. Every time you mention a specific latency target, a concrete cache eviction policy, or an actual battery impact number, you're telling the interviewer: I've shipped this. I've seen it break. I know how to build it right.

That's the signal they're hiring for.