Meta Mid-Level Mobile Developer Interview Preparation Guide
Meta's interview process for Mid-Level Mobile Developers consists of a recruiter screening phase, followed by a technical phone screen, and concludes with 4-5 onsite interview rounds lasting a full day. The process evaluates core algorithmic problem-solving, mobile-specific technical knowledge, system design for scalable mobile applications, and cultural alignment with Meta's values. All technical interviews include coding components with emphasis on communication, debugging, and verification of solutions. The process typically spans 4-6 weeks from initial application to offer.
Interview Rounds
Recruiter Screening
What to Expect
This is a 30-minute phone call with a Meta recruiter to assess your background fit for the role. The recruiter will review your resume, discuss your interest in Meta and the Mobile Developer position, explore your relevant experience with iOS and/or Android development, and answer your questions about the role and company. This is a preliminary screening to ensure basic qualifications are met before proceeding to technical interviews.
Tips & Advice
Be genuine and enthusiastic about Meta's mobile products. Have a clear 2-minute summary of your mobile development background and why you're interested in Meta specifically. Mention specific Meta mobile products you use and appreciate. Ask thoughtful questions about the team, mobile strategy, or technical challenges. Smile while speaking (interviewers can hear it in your voice). Be ready to discuss your availability and any visa sponsorship needs.
Focus Topics
Knowledge of Meta's Mobile Ecosystem
Understand Meta's mobile products: Facebook, Instagram, WhatsApp, Threads, and Meta Quest. Know which platforms they serve (iOS, Android, or both), and recent features or challenges they've announced.
Practice Interview
Study Questions
Project Ownership and Impact
Prepare 2-3 examples of mobile projects where you took ownership, drove decisions, and delivered measurable impact. For mid-level, emphasize cross-functional collaboration and mentoring involvement.
Practice Interview
Study Questions
Motivation for Meta and Mobile Role
Develop a compelling narrative about why you want to work at Meta specifically and why mobile development excites you. Reference Meta's mobile products, engineering culture, or technical challenges.
Practice Interview
Study Questions
Background and Mobile Development Experience
Articulate your experience with iOS development (Swift/Objective-C), Android development (Kotlin/Java), and/or cross-platform frameworks (React Native, Flutter). Emphasize projects you've owned and impact you've delivered.
Practice Interview
Study Questions
Technical Phone Screen
What to Expect
This 45-60 minute phone interview focuses on core data structures, algorithms, and problem-solving ability. You'll receive 1-2 coding problems to solve in real-time using a shared code editor (CoderPad or similar). The interviewer will assess how you approach unfamiliar problems, ask clarifying questions, communicate your thought process, and verify your solutions. While not mobile-specific, this round tests foundational algorithmic thinking essential for mobile development.
Tips & Advice
Ask clarifying questions for 1-2 minutes before coding—understand input constraints, edge cases, and performance requirements. Narrate your approach verbally before writing code; interviewers evaluate thinking, not just the final answer. Write clean, readable code with clear variable names. Test your code mentally against basic cases, edge cases (empty inputs, single elements, negatives), and stress cases (very large inputs). If stuck, explain your best approach, ask for hints, and iterate. Meta values candidates who debug methodically rather than freeze. Discuss time and space complexity in Big O notation. Ask if your solution can be optimized and propose improvements if time permits.
Focus Topics
Linked Lists and Node Manipulation
Understand pointer/reference manipulation, traversal, and state management. Practice cycle detection, reversal, merging, and insertion/deletion in linked lists.
Practice Interview
Study Questions
Graphs, BFS, and DFS
Understand graph representations (adjacency list, adjacency matrix), depth-first search, breadth-first search, cycle detection in directed/undirected graphs, and topological sorting.
Practice Interview
Study Questions
Binary Trees and Tree Algorithms
Solve problems involving tree traversal (inorder, preorder, postorder), tree properties (height, depth, balance), lowest common ancestor, path sums, and tree construction.
Practice Interview
Study Questions
Hash Maps and Hash Sets
Use hash-based data structures to optimize solutions. Understand time/space tradeoffs and when to choose hashing over sorting or nested loops.
Practice Interview
Study Questions
Arrays and Strings Algorithms
Master problems involving array manipulation, substring search, pattern matching, and string transformations. This includes problems like longest substring without repeating characters, two-sum variants, and array rotation.
Practice Interview
Study Questions
Communication and Problem-Solving Process
Practice articulating your approach before coding, asking clarifying questions, explaining your decisions, and walking through test cases verbally. Learn to adapt when constraints change mid-interview.
Practice Interview
Study Questions
Mobile Technical Interview 1: iOS Development
What to Expect
This 45-50 minute onsite interview focuses on iOS-specific development skills including Swift/Objective-C, UIKit/SwiftUI fundamentals, app lifecycle, memory management, concurrency, and mobile-specific design patterns. You may solve coding problems with an iOS lens, discuss architecture decisions, or implement features using platform APIs. The interviewer assesses your depth of iOS knowledge and ability to apply it to real mobile challenges.
Tips & Advice
Come prepared with concrete examples of iOS apps you've shipped or contributed to significantly. Be ready to explain architectural decisions you made (MVC, MVVM, VIPER, Clean Architecture) and trade-offs. Discuss how you handle memory in iOS (strong/weak references, retain cycles, ARC). Prepare for questions about async/await, Combine framework, or GCD depending on how current your experience is. If asked to code an iOS feature, think about app lifecycle, threading, and user experience. Mention your experience with app store deployment, TestFlight, and submission guidelines. For mid-level, emphasize how you've mentored junior iOS developers or improved code quality on your team.
Focus Topics
Concurrency, Threading, and Async Patterns
Understanding of GCD (Grand Central Dispatch), DispatchQueue, async/await syntax, Combine framework, and how to avoid main thread blocking. Knowledge of deadlocks, race conditions, and thread safety.
Practice Interview
Study Questions
Memory Management and Performance Optimization
Understanding of ARC, strong/weak/unowned references, retain cycles, and memory leaks. Ability to profile apps using Xcode Instruments (Allocations, Leaks, Time Profiler) and optimize performance for mobile constraints (battery, RAM, network).
Practice Interview
Study Questions
iOS App Lifecycle and State Management
Understanding of UIViewController lifecycle, view controller transitions, state restoration, saved state APIs, and how to handle app suspend/resume scenarios. Knowledge of SceneDelegate for multi-window support on iPad.
Practice Interview
Study Questions
iOS App Architecture and Design Patterns
Knowledge of MVC, MVVM, and VIPER architectures. Understanding of dependency injection, reactive programming (RxSwift/Combine), and separation of concerns. Ability to discuss trade-offs between architectural approaches.
Practice Interview
Study Questions
Swift Language Fundamentals and Modern iOS Development
Deep understanding of Swift syntax, optionals, error handling, and modern Swift features. Familiarity with recent Swift versions and how they improve iOS development. Understanding of when to use protocols, extensions, and generics in iOS apps.
Practice Interview
Study Questions
Mobile Technical Interview 2: Android Development
What to Expect
This 45-50 minute onsite interview focuses on Android-specific development skills including Kotlin/Java, Android lifecycle, fragments, services, background processing, and mobile-specific design patterns. You may solve problems with Android context, discuss system design for scalable Android apps, or implement Android features using platform APIs. The interviewer assesses your Android development depth and how you apply platform knowledge to real challenges. This round mirrors the iOS interview but tests Android expertise.
Tips & Advice
Showcase Android apps you've shipped, particularly any with complex UIs, background tasks, or tricky lifecycle scenarios. Be fluent in Kotlin syntax and explain why you prefer it over Java (or vice versa if applicable). Discuss your experience with modern Android architecture (MVVM with LiveData/StateFlow, Clean Architecture, MVI). Be ready to explain Activity/Fragment lifecycle in detail and common lifecycle-related bugs you've fixed. Discuss how you handle background work (WorkManager, Service, IntentService) without draining battery. Mention experience with dependency injection (Hilt, Dagger) and testing frameworks. For mid-level, explain how you've improved Android code quality or mentored junior Android developers on your team.
Focus Topics
Android Architecture: MVVM, Clean Architecture, StateFlow
Practical experience with modern Android architecture patterns including MVVM, Clean Architecture, and reactive state management using LiveData/StateFlow. Understanding of separation between UI, ViewModel, and Repository layers.
Practice Interview
Study Questions
Background Work and Battery Optimization
Knowledge of WorkManager, Service, and when to use each. Understanding of Doze mode, battery optimization, and how to minimize battery drain from background operations. Familiarity with permissions model and runtime permission handling.
Practice Interview
Study Questions
Android Concurrency with Coroutines
Understanding of coroutines, suspend functions, structured concurrency, context switching, and Job/Scope management. Knowledge of Dispatchers (Main, IO, Default) and when to use each. Understanding of cancellation and exception handling in coroutines.
Practice Interview
Study Questions
Kotlin Language and Java Interoperability
Fluency in Kotlin syntax including extension functions, coroutines, null safety, and data classes. Understanding of Java interoperability for legacy codebases. Knowledge of when Kotlin features improve code expressiveness and maintainability.
Practice Interview
Study Questions
Android Lifecycle and Fragment Management
Deep understanding of Activity/Fragment lifecycle methods, state management across lifecycle transitions, SavedStateHandle, ViewModel scope, and how to handle configuration changes gracefully. Knowledge of multi-fragment navigation.
Practice Interview
Study Questions
System Design Interview: Scalable Mobile Architecture
What to Expect
This 45-50 minute onsite interview evaluates your ability to design scalable mobile systems at an architectural level. Rather than designing large distributed systems like backend engineers, you'll focus on mobile-specific challenges: designing offline-first mobile apps, scaling real-time features for millions of users, handling network reliability, caching strategies, data synchronization, and mobile SDK architecture. The interviewer assesses how you balance mobile constraints (battery, memory, connectivity) with feature requirements. You might discuss designing a mobile messaging feature, real-time notification system, or offline data sync mechanism.
Tips & Advice
Ask clarifying questions first: How many users? How often do they interact? What devices/networks? Then propose a solution architecture. For mobile, always discuss trade-offs between user experience and resource constraints. Talk about caching (in-memory, disk, server), network optimization (batching, compression, retry logic), and offline behavior. Discuss data synchronization—how do you handle conflicts when users work offline? Mention real examples from Meta products (Instagram Stories, Facebook Messenger). Sketch diagrams on the whiteboard or virtual board to visualize your architecture. Discuss testing strategies for unreliable networks and resource constraints. For mid-level, explain how you'd mentor a junior engineer on these trade-off decisions.
Focus Topics
Mobile SDK Architecture and API Design
Designing SDKs for mobile platforms: clean APIs, backwards compatibility, version management, and minimizing SDK size and performance impact. Understanding of shared libraries, binary size optimization.
Practice Interview
Study Questions
Caching Strategies and Memory Management
Multi-layer caching: in-memory caches (LRU, TTL-based), disk caches (image caches, API response caches), and CDN/server-side caching. Understanding of cache invalidation, expiration policies, and balancing memory constraints.
Practice Interview
Study Questions
Real-Time Features and Push Notifications
Designing systems for real-time updates: push notification architecture, WebSockets/polling trade-offs, handling notification delivery reliability, and user experience patterns for real-time events.
Practice Interview
Study Questions
Offline-First Architecture and Data Synchronization
Design mobile apps that work offline and sync when connectivity returns. Understand conflict resolution, eventual consistency, and how to prevent data loss. Knowledge of local databases (SQLite, Realm, Room) and sync patterns.
Practice Interview
Study Questions
Mobile Network Optimization and Reliability
Strategies for minimizing network requests, batching operations, using compression, handling slow/unreliable networks, implementing exponential backoff retry logic, and request prioritization. Understanding of HTTP/REST vs gRPC trade-offs for mobile.
Practice Interview
Study Questions
Behavioral and Culture Fit Interview
What to Expect
This 40-45 minute onsite interview assesses how well you align with Meta's values, work style, and culture. The interviewer will ask behavioral questions using the STAR format (Situation, Task, Action, Result) to understand how you've handled challenges, collaborated with teams, resolved conflicts, and demonstrated leadership. Topics typically include: how you've owned projects, dealt with ambiguity, handled failure, mentored or influenced others, and navigated disagreements. This round evaluates your growth mindset, collaboration style, and fit for Meta's fast-paced, execution-oriented culture.
Tips & Advice
Prepare 5-6 STAR format stories covering: a project you owned end-to-end, a time you mentored someone, a conflict you resolved, a failure you learned from, a time you drove change, and a time you collaborated cross-functionally. Use specific examples with measurable outcomes. For mid-level, emphasize ownership, mentoring, and influence over just task completion. Practice articulating what you learned and how it shaped your approach. Research Meta's values (Move Fast, Be Bold, Focus on Impact, Build What Matters) and weave these into your stories naturally. Be authentic—interviewers can detect coached responses. Listen carefully to follow-up questions and provide additional detail. Ask thoughtful questions about the team, challenges, and career growth at Meta.
Focus Topics
Cross-Functional Collaboration and Conflict Resolution
Describe a situation where you worked with designers, backend engineers, or product managers toward a common goal. Explain how you aligned perspectives, resolved disagreements, and maintained relationships. Emphasize finding win-win solutions.
Practice Interview
Study Questions
Mentoring and Growing Other Engineers
Provide examples of how you've helped junior developers grow, given effective feedback, and elevated their skills. Discuss code reviews, pair programming, design discussions, or formal mentoring. Explain your philosophy on mentoring.
Practice Interview
Study Questions
Learning from Failure and Technical Debt
Share an example of a technical decision that didn't work out, a project that shipped with issues, or a time you underestimated complexity. Explain what you learned and how you'd approach it differently. Show accountability and growth.
Practice Interview
Study Questions
Handling Ambiguity and Driving Decisions
Describe a situation where requirements were unclear or multiple solutions existed. Explain how you gathered information, proposed a direction, got buy-in, and moved forward. Emphasize your decision-making process and communication.
Practice Interview
Study Questions
End-to-End Project Ownership
Demonstrate ownership of medium-sized mobile projects from conception through deployment. Discuss how you drove decisions, removed blockers, collaborated across teams (design, backend, QA), and delivered impact. For mid-level, emphasize technical leadership and how you shaped the project direction.
Practice Interview
Study Questions
Frequently Asked Mobile Developer Interview Questions
What is the difference between a race condition and a data race? Give a small example of each, and explain why code can be free of data races and still contain a race condition.
Sample Answer
Direct answer
A data race is a memory-level fact: two threads access the same memory location at the same time with no ordering between them, at least one access is a write, and the accesses are not both atomic (an atomic access is one the language guarantees happens as a single indivisible step that other threads see either completely or not at all, such as a C11 atomic_int operation; a plain int access is not). In C and C++ the language definition makes the whole program's behaviour undefined if one occurs. A race condition is a logic-level fact: the program's correctness depends on the timing or interleaving of operations, so some interleavings produce a wrong result. A program can have either without the other: you can lock every single access (no data race) and still check a balance in one critical section and withdraw in another (race condition).
The precise definitions
In plain words for the next paragraph: an evaluation is one execution of an expression, such as a read or a write of a variable; a memory location is a variable (or a distinct piece of one) that has its own address; and undefined behaviour means the language places no limit on what the program does. cppreference's statement of the C++ memory model says two evaluations conflict if one modifies a memory location and the other reads or modifies the same location, and a program with two conflicting evaluations has a data race unless both run on the same thread (or in the same signal handler), both are atomic operations, or one happens-before the other. It also says: "If a data race occurs, the behavior of the program is undefined." (Happens-before is the ordering a mutex unlock/lock pair, a thread join, or an atomic release/acquire pair gives you: everything before the unlock is visible after the matching lock.)
| Data race | Race condition | |
|---|---|---|
| Level | Memory accesses (language rule) | Program logic (an invariant about outcomes) |
| Definition | Conflicting accesses, no happens-before, not both atomic | Outcome depends on timing of operations |
| Consequence in C/C++ | Undefined behaviour (compiler may assume it never happens) | A wrong but well-defined result |
| Found by | Race detectors such as ThreadSanitizer (TSan) | Reasoning about invariants, stress tests, review |
| Fixed by | Making accesses atomic or ordering them with a lock | Making the check and the act one atomic step |
Worked example: both bugs in one program, run in a Linux container
One file, two parts. Part 1 is a data race (two threads increment a plain int). Part 2 is a race condition with no data race: every access to balance is under a mutex, but the check and the withdrawal are separate critical sections. A barrier forces both threads to finish their check before either acts, so the bad interleaving happens on every run instead of rarely.
#include <pthread.h>
#include <stdio.h>
/* Part 1: a data race. Two threads write the same int with no synchronization. */
static int hits = 0;
static void *bump(void *arg) { (void)arg; for (int i = 0; i < 100000; i++) hits++; return NULL; }
/* Part 2: a race CONDITION with no data race. Every access is under the mutex,
but check and act are in separate critical sections. */
static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static pthread_barrier_t both_checked;
static int balance = 100;
static int withdraw(int amt) {
pthread_mutex_lock(&m);
int ok = balance >= amt; /* check */
pthread_mutex_unlock(&m);
pthread_barrier_wait(&both_checked); /* force the bad interleaving for the demo */
if (!ok) return 0;
pthread_mutex_lock(&m);
balance -= amt; /* act on a stale check */
pthread_mutex_unlock(&m);
return 1;
}
static void *w(void *arg) { (void)arg; withdraw(80); return NULL; }
int main(void) {
pthread_t a, b;
pthread_create(&a, NULL, bump, NULL); pthread_create(&b, NULL, bump, NULL);
pthread_join(a, NULL); pthread_join(b, NULL);
printf("hits = %d (expected 200000)\n", hits);
pthread_barrier_init(&both_checked, NULL, 2);
pthread_create(&a, NULL, w, NULL); pthread_create(&b, NULL, w, NULL);
pthread_join(a, NULL); pthread_join(b, NULL);
printf("balance = %d (should never be negative)\n", balance);
return 0;
}
Compiled and run with gcc:14 (GCC 14.4, aarch64 Linux container), five runs of gcc -O1 -pthread race.c -o r && ./r (each run prints both lines; the hits line is timing-dependent, so your five runs will differ):
hits = 100000 (expected 200000)
balance = -60 (should never be negative)
hits = 100000 (expected 200000)
balance = -60 (should never be negative)
hits = 100000 (expected 200000)
balance = -60 (should never be negative)
hits = 200000 (expected 200000)
balance = -60 (should never be negative)
hits = 100000 (expected 200000)
balance = -60 (should never be negative)
Note the fourth run: the plain int race printed the "right" answer once. That is the point of the next paragraph: the program is wrong on every run, and only the symptom varies.
Why 100000 in four of five runs at -O1? A register is a storage cell inside the CPU that is much faster than memory. The compiler is allowed to assume no data race, so it kept the counter in a register (it hoisted the load and store out of the loop, meaning it moved them before and after the loop instead of repeating them every iteration): the generated bump loads hits once, adds 100000, and stores once (I read the gcc -O1 -S output: one ldr, one str, a register loop between them). Walk through it with illustrative timing: thread A loads hits (0) into a register and adds 1 a hundred thousand times in the register, so the register holds 100000, then stores 100000 to memory; thread B did the same, starting from its own load of 0, and stores 100000 too. If the two threads overlap, both loaded 0, so whichever store lands last writes the same 100000 and one thread's whole contribution is overwritten. If one thread happens to finish completely before the other starts (thread start-up is slow compared with a 100000-iteration register loop, but it can happen, as in the fourth run), the second loads 100000 and stores 200000 and the symptom disappears for that run. So the result is 100000 or 200000 depending on timing, never a reliable total. In the ldr/str reading, ldr is the Arm instruction that loads from memory into a register and str stores a register back, so seeing only one of each around the loop is the evidence that the counter lived in the register. At -O0 the same program loses a variable amount: three runs printed 118833, 135721 and 125027. Both are "correct" outcomes of undefined behaviour, which is the practical meaning of UB: you cannot predict the symptom.
Under TSan (gcc -O1 -g -fsanitize=thread -pthread race.c -o rt && ./rt) the first part is reported as a data race at race.c:6 in bump (a read and a previous write by the two threads) and the count prints 200000, because instrumentation changes the code the compiler generates. A TSan data-race report names the kind of problem, then gives two blocks, one for the access that triggered it (read or write, its thread, its function and file:line) and one for the earlier conflicting access in the other thread, plus the location of the variable; two accesses to one address from two threads with no ordering between them is the whole diagnosis. The summary line is ThreadSanitizer: reported 1 warnings. Part 2 produced balance = -60 in that run too, and TSan said nothing about it: the accesses are all ordered by the mutex, so there is no data race to report. TSan's documentation describes the tool as one that "detects data races", so a clean TSan run says nothing about check-then-act bugs.
Why -60: both withdrawals checked 100 >= 80 before either subtracted, then 100 - 80 - 80 = -60. The fix is to make check and update one critical section (or a compare-and-swap loop on the balance):
lock; if (balance >= amt) balance -= amt, ok = 1; unlock
Why code can be free of data races and still be wrong
Synchronizing each variable protects the memory, not the rule that relates several operations. Typical shapes: check-then-act (above), read-modify-write split across two lock regions, "if absent then insert" on a locked map, two separately atomic variables that must change together, and iterating a collection whose size was read earlier.
The same race between a task and an interrupt service routine (ISR)
A counter updated by both a task and an ISR is the same defect. volatile stops the compiler from caching the variable, but cppreference notes that volatile access is suitable for communication with a signal handler, "but not with another thread of execution". count++ is still a load, an add and a store, and an ISR that fires between the load and the store loses an update; the fix is a short critical section (a stretch of code only one thread or interrupt handler may be inside at a time) with interrupts masked, or a hardware atomic read-modify-write where the core has one.
Trade-offs and pitfalls
- "Benign" data races do not exist in C/C++: even a racy flag can be hoisted out of a loop, as the register-resident counter above shows.
- Removing a data race with atomics does not fix a race condition; ask what invariant ties the operations together.
- Do not rely on a test passing: with a two-instruction window, a failure may need millions of iterations; use TSan for data races and invariant checks plus forced interleavings (as the barrier does) for logic races.
Explain what a cache is and why mobile applications use caching. In your answer include concrete examples of benefits (reduced latency, lower network/data usage, improved battery life), common cache locations in mobile apps (HTTP layer, in-memory, disk-based caches, secure storage), and a short checklist a mobile developer can use to decide whether to cache a particular resource.
Sample Answer
What a cache is
A cache is a smaller, faster storage layer that holds a copy of data so a later request for that data can be served without repeating the expensive work (a network call, a database query, a slow computation) that produced it the first time. The trade-off it accepts is that the copy can become out of date (stale) if the original changes.
Why mobile apps cache
Mobile apps sit at the far end of an unreliable, metered, battery-powered network path, so avoiding a repeat network round trip pays off more than on a desktop web app:
- Reduced latency: a cached local read takes single-digit milliseconds versus 200ms or more for a cellular round trip, so a cached screen renders instantly instead of showing a spinner.
- Lower network/data usage: re-serving a cached product image or API response instead of re-downloading it saves the user's data plan, which matters on capped or pay-per-MB plans.
- Improved battery life: the radio (the cellular/Wi-Fi chip) is one of the most power-hungry components, and every network request wakes it up. Serving from cache avoids that wake-up entirely.
- Offline resilience: a cached response lets the app show something useful (a stale product list) instead of an error screen when connectivity drops in an elevator or subway.
Common cache locations in a mobile app
- HTTP layer: an HTTP client cache (OkHttp's disk cache on Android, URLCache on iOS) that respects Cache-Control/ETag headers from the server, so repeat requests to the same endpoint are short-circuited automatically.
- In-memory: an LRU (least-recently-used, evicts the entry that was accessed longest ago) map, such as Android's LruCache, holding decoded images or parsed objects for the current session. Fastest option, but wiped when the app is killed.
- Disk-based: a local database (Room/SQLite, Core Data) or file cache for structured data or large media that should survive an app restart.
- Secure storage: Keychain on iOS. On Android, do NOT reach for
EncryptedSharedPreferences(fromandroidx.security:security-crypto) in new code: Google deprecated that library, includingEncryptedSharedPreferencesandEncryptedFile, in 2025 without shipping a stable successor API. The current recommended path is to generate and hold the encryption key in the Android Keystore system directly (hardware-backed on supported devices), encrypt the value yourself (AES-GCM) with that key, and store only the resulting ciphertext in a plainSharedPreferencesentry or Jetpack DataStore, for sensitive cached values like an auth token. Never use secure storage for bulk content, regardless of which approach backs it, since it is slower and size-limited.
A short checklist before caching a resource
- Is it read far more often than it changes (a product catalog, a user's own profile), versus something that must always be live (a live auction price, a one-time payment confirmation)?
- Is a slightly stale value acceptable, or does staleness cause a real error (double-booking a seat)?
- Is the data big enough to matter for battery/data savings, or so big it would blow the app's memory/disk budget?
- Does it contain sensitive data that needs secure storage and a short TTL (time-to-live, how long a cached value is kept before it is treated as expired), rather than a plain disk cache?
- Is there a clear invalidation trigger (a push notification, a version bump, a TTL) so the cache does not go silently stale forever?
Write a Go function (or clear pseudocode) that consumes an input stream of push messages and emits provider-specific batches to APNs and FCM while respecting each provider's per-connection send rate and max batch sizes. Requirements: prioritize high-priority messages, implement exponential retry with jitter for transient failures (including handling 429s), and be able to run multiple worker instances. Keep the implementation concise but include key structures and algorithms.
Sample Answer
Approach (brief)
- Use a single input channel for incoming push messages; workers pull, group by provider and priority, emit batches constrained by per-connection rate and max batch size.
- Use token-bucket rate limiter per provider and per connection; exponential backoff with full jitter for transient errors (including 429).
- Support multiple worker instances by keeping state local to a worker; idempotency handled by message IDs.
Key structs & constants
type Provider int
const (
APNS Provider = iota
FCM
)
type Message struct {
ID string
Provider Provider
Priority int // higher => send first
Payload []byte
}
type Batch struct {
Provider Provider
Messages []*Message
}
Worker loop (Go-like pseudocode)
func Worker(in <-chan *Message, sendAPNS func(*Batch) error, sendFCM func(*Batch) error, maxBatch int, rateLimiter *rate.Limiter) {
// local priority queues by provider
pq := make(map[Provider]*PriorityQueue)
backoff := make(map[string]time.Duration)
tick := time.NewTicker(100 * time.Millisecond)
defer tick.Stop()
for {
select {
case m := <-in:
pq[m.Provider].Push(m)
case <-tick.C:
for _, prov := range []Provider{APNS, FCM} {
limiter := rateLimiterFor(prov) // token-bucket per provider
if limiter.AllowN(time.Now(), 1) == false { continue }
batch := &Batch{Provider: prov}
for i:=0; i<maxBatch; i++ {
if msg := pq[prov].PopHighestPriority(); msg!=nil { batch.Messages = append(batch.Messages, msg) } else { break }
}
if len(batch.Messages)==0 { continue }
go func(b *Batch) {
var err error
if b.Provider==APNS { err = sendAPNS(b) } else { err = sendFCM(b) }
if err!=nil {
// treat 429 or transient as retry with backoff+jitter
for _, msg := range b.Messages {
d := backoff[msg.ID]
if d==0 { d = 100 * time.Millisecond }
// exponential
d = time.Duration(float64(d) * 2)
// cap
if d > 30*time.Second { d = 30*time.Second }
// full jitter
wait := time.Duration(rand.Int63n(int64(d)))
backoff[msg.ID] = d
go func(m *Message, w time.Duration) {
time.Sleep(w)
in <- m // requeue
}(msg, wait)
}
} else {
// success: remove backoff entries
for _, msg := range b.Messages { delete(backoff, msg.ID) }
}
}(batch)
}
}
}
}
Explanation & notes
- PriorityQueue orders by Priority then arrival time.
- rate.Limiter (golang.org/x/time/rate) enforces per-connection send-rate; create pool of connections per provider if needed.
- Exponential backoff with full jitter prevents thundering herd; 429 treated as transient.
- Multiple worker instances: run many Workers reading from same durable queue (e.g., Redis stream, Kafka) to scale horizontally and avoid duplicate delivery by using message IDs and idempotent provider APIs.
- Edge cases: long-lived backlog, poison messages (add retry limit and DLQ), provider-specific per-message errors (map responses per-message).
Tell me about a time you had to give difficult feedback to a teammate or partner you worked with closely. What made the conversation hard, how did you frame it, and what happened afterward?
Sample Answer
Situation: I worked closely with a partner who was strong technically but often changed direction late, which was creating churn for the rest of the team.
Task: I needed to give difficult feedback without damaging trust.
Action: I chose a private conversation and used the SBI format, which means Situation, Behavior, Impact. I said, "In yesterday's planning meeting, when the scope changed after we had already aligned with design, it created rework and made the team less confident in the plan." I kept the tone factual, then asked what was driving the change. It turned out they were reacting to pressure from another stakeholder and had not surfaced it earlier. We agreed they would flag uncertainty sooner and bring changes through planning instead of in the middle of execution.
Result: The conversation was uncomfortable, but it improved our working relationship because it was specific and fair. Their behavior became more predictable, and the team trusted them more because expectations were clearer.
Compare LiveData, Kotlin Flow and RxJava for representing streams of asynchronous data in Android. Discuss lifecycle-awareness, hot vs cold streams, backpressure handling, integration with ViewModel and UI threading, and when you'd prefer one over the others.
Sample Answer
Overview
LiveData, Kotlin Flow and RxJava each model async streams but target different needs: LiveData is Android-UI-centric, Flow is Kotlin-coroutine-native, RxJava is feature-rich reactive library.
Lifecycle-awareness
- LiveData: built-in lifecycle-aware — observers auto-start/stop with LifecycleOwner (Activity/Fragment).
- Flow: not lifecycle-aware by default; use lifecycleScope and lifecycle.repeatOnLifecycle to collect safely.
- RxJava: not lifecycle-aware; requires manual disposal or use AutoDispose / Lifecycle-aware extensions.
Hot vs Cold
- LiveData: hot — keeps latest value; observers receive current state.
- Flow: by default cold (Flows start on collect). SharedFlow/StateFlow provide hot behavior (StateFlow ~ LiveData).
- RxJava: supports both; Observable/Flowable can be hot or cold depending on source and Subjects.
Backpressure
- LiveData: no backpressure semantics — suitable for UI events/state.
- Flow: has structured backpressure via suspending emissions and operators (buffer, conflate, collectLatest).
- RxJava: Flowable offers backpressure strategies; Observable lacks built-in backpressure.
ViewModel & UI threading
- LiveData: pairs naturally with ViewModel; postValue/setValue handle main thread concerns.
- Flow: use viewModelScope to emit on Dispatchers.IO and collect on Dispatchers.Main; operators like flowOn, withContext manage threads.
- RxJava: use subscribeOn/observeOn; integrate with ViewModel but manage disposables.
When to prefer
- LiveData: simple UI state, minimal setup, lifecycle safety (small apps).
- Flow/StateFlow: coroutine-native, testable, better for complex async pipelines, prefer when using coroutines.
- RxJava: existing Rx-heavy codebase or need advanced operators, multicasting, backpressure control; otherwise Flow is lighter for new Android projects.
Example: for continuous sensor data with sampling and backpressure -> Flow with buffer/conflate or RxJava Flowable; for UI form state -> LiveData or StateFlow.
Implement from-scratch a simple hash table in Python supporting put(key, value), get(key), and delete(key) using open addressing (linear probing). Support resizing when load factor exceeds a threshold. Explain complexity, deletion special cases (tombstones), and how resizing preserves correctness.
Sample Answer
Direct answer
A hash table built with open addressing stores every entry directly in one flat array; on a
collision it probes forward (here, linearly) to the next slot, uses a tombstone marker (not a bare
None) for deleted entries so later probes don't stop early, and resizes once the load factor
crosses a threshold.
Structured elaboration
Three distinct slot states, not two. A naive implementation might track only "empty" and
"occupied," but deletion needs a third state: tombstone (previously occupied, now deleted). A
probe sequence must treat a tombstone as "keep looking, something later might still be the key I
want" while treating true emptiness as "stop, the key definitely isn't here." Confusing these two is
the single most common open-addressing bug: without tombstones, deleting an entry mid-probe-sequence
can make a later, still-present key silently unreachable.
Insertion. Probe from hash(key) % capacity, remembering the first tombstone slot seen (so a
successful insert reuses it rather than always walking to a brand-new empty slot); stop at the key
itself (update in place) or at a true empty slot (place there, using the remembered tombstone slot
if one was seen along the way).
Resizing. The load factor check should count tombstones toward the trigger (not just live
entries), since a table full of tombstones is just as slow to probe as one full of live entries, and
a resize is exactly what clears all tombstones out (it rebuilds the table from scratch with only the
live entries, since insertion during a resize never needs to place a tombstone).
Worked example
_TOMBSTONE = object()
class OpenAddressingHashMap:
def __init__(self, initial_capacity=8):
self._capacity = initial_capacity
self._used = 0
self._keys = [None] * self._capacity
self._values = [None] * self._capacity
def _probe(self, key):
idx = hash(key) % self._capacity
first_tombstone = None
for _ in range(self._capacity):
slot = self._keys[idx]
if slot is None:
return (first_tombstone if first_tombstone is not None else idx), False
if slot is _TOMBSTONE:
if first_tombstone is None:
first_tombstone = idx
elif slot == key:
return idx, True
idx = (idx + 1) % self._capacity
return first_tombstone, False
def put(self, key, value):
if self._used / self._capacity >= 0.7:
self._resize(self._capacity * 2)
idx, found = self._probe(key)
if not found and self._keys[idx] is None:
self._used += 1
self._keys[idx] = key
self._values[idx] = value
def get(self, key):
idx, found = self._probe(key)
return self._values[idx] if found else None
def delete(self, key):
idx, found = self._probe(key)
if not found:
return False
self._keys[idx] = _TOMBSTONE
self._values[idx] = None
return True
def _resize(self, new_capacity):
old_keys, old_values = self._keys, self._values
self._capacity, self._used = new_capacity, 0
self._keys, self._values = [None]*new_capacity, [None]*new_capacity
for k, v in zip(old_keys, old_values):
if k is not None and k is not _TOMBSTONE:
self.put(k, v)
t = OpenAddressingHashMap(initial_capacity=4)
t.put("a", 1); t.put("b", 2); t.put("c", 3)
print("capacity after 3 inserts:", t._capacity)
print(t.get("a"), t.get("b"), t.get("c"))
t.delete("b")
print("get('b') after delete:", t.get("b"))
t.put("d", 4) # reuses b's tombstone slot
print(t.get("d"), t.get("a"), t.get("c"))
for i in range(20):
t.put(f"k{i}", i)
print("capacity after 20 more inserts:", t._capacity)
print("k17 ->", t.get("k17"))
Running this prints: capacity after 3 inserts: 4 (2/4 = 0.5 was below the 0.7 threshold when "c" was
inserted, so no resize fired yet), then 1 2 3 (all three retrievable), then
get('b') after delete: None (confirms deletion), then 4 1 3 ("d" correctly reused "b"'s freed
slot while "a" and "c" remained untouched), then capacity after 20 more inserts: 32 (several
doublings: 4 -> 8 -> 16 -> 32 as load crossed 0.7 repeatedly), then k17 -> 17 (every key survived
all those resizes correctly).
Trade-offs and pitfalls
The two bugs that actually break this in practice: (1) clearing a deleted slot to None instead of
a tombstone, which silently breaks lookups for any key whose probe sequence passed through that slot;
(2) checking load factor using only live entries (ignoring tombstones), which lets a
delete-heavy workload accumulate tombstones until probing degrades toward O(capacity) even though the
live entry count looks small, exactly why the trigger above uses _used (live entries and
tombstones combined), not a live-only count.
Tell me about a time you noticed and fixed a technical problem, bug, or process gap that was outside your assigned scope, without being asked. Walk me through how you discovered it, the concrete steps you took to fix or improve it end to end, who you kept informed, and the measurable outcome.
Sample Answer
Direct answer
Notice it during normal work rather than a special investigation, scope a fix small enough to do without asking, fix the actual root cause rather than the symptom, and tell the team what changed and why so the fix is visible rather than buried in a commit.
Structured elaboration
- Discovery usually happens incidentally: while doing assigned work, you hit something off, a flaky test everyone re-runs, a script everyone has a manual workaround for, that is outside your actual ticket.
- Scope the fix to what you can safely do inside your own access and time without needing approval, because the change is small, contained, and does not touch anyone else's active work.
- Fix the actual cause: spend the extra time to find why it is happening rather than patching the symptom, re-running until it passes, adding a longer timeout, which just delays the same failure.
- Keep people informed: a short note in the team channel or pull-request description saying what was broken and what changed, so teammates do not independently rediscover the same thing later.
- Measurable outcome: something concrete that changed as a result, fewer reruns needed, less time lost per week, even if the number is modest.
Worked example
A shared integration test suite failed intermittently, and the team's habit was to re-run the whole suite until it passed, costing a few extra minutes each time and adding up across a team running it many times a day. Digging in during downtime between tickets uncovered the real cause: two tests shared a test fixture, a piece of setup data, and ran in a nondeterministic order, so one test occasionally read data the other had not finished writing, a race condition, a bug where the outcome depends on timing that is not guaranteed. Isolating the fixture per test so each test owned its own data, the fix went out as a small pull request with the root cause noted in the description, so reviewers understood it was not just a timeout bump. After the fix, the suite's flaky reruns, which had been happening roughly half a dozen times a day across the team before the fix, dropped from that regular daily occurrence to about once every couple of weeks over the following month.
Trade-offs and pitfalls
A common wrong turn is patching the symptom, retry logic, a longer timeout, because it is faster, which hides the same bug until it resurfaces somewhere less convenient. Another is fixing it silently without telling anyone, so the team never learns what was actually wrong and cannot recognize the same pattern next time. Also watch scope: a fix this small does not need permission, but if it had required touching shared infrastructure other teams depended on, the right move would have been to flag it first rather than just changing it.
You're asked to set up a lightweight mentorship structure for a small team. What would you actually put in place, pairing, cadence, shared resources, and how would you keep it low-overhead?
Sample Answer
Direct answer
A lightweight structure needs three ingredients: a small, predictable time commitment (a fixed cadence, not open-ended availability), a place where knowledge accumulates outside people's heads, and two or three signals you actually look at instead of a heavy program. Keep it low-overhead by reusing rituals the team already has, like code review, rather than inventing new meetings.
Structured elaboration: the components
| Component | What you set up | Why it stays lightweight |
|---|---|---|
| Pairing and cadence | One small recurring block per pair (for example, a single weekly slot), rotating pairs on a short cycle so everyone gets exposure | Bounded time commitment, predictable, no ad hoc scheduling |
| Shared knowledge base | One folder or doc space with a couple of templates (session notes, a troubleshooting or FAQ page), edited through the team's existing review flow | No new tool to learn or separately maintain |
| Kickoff, not a training program | One short session covering what makes a good mentoring conversation and a few question prompts | One-time cost, not ongoing overhead |
| Signals you track | Two or three only, checked occasionally: are sessions actually happening, is the knowledge base getting used, do people feel less stuck | Avoids the program itself becoming the overhead |
Worked example
For a four-person team, a three-week rotation covers every unique pair exactly once: week one pairs A-B and C-D, week two pairs A-C and B-D, week three pairs A-D and B-C, then the cycle repeats. If each pairing block is 45 minutes, the weekly time cost per person is one session, 45 minutes, or 0.75 hours a week, plus roughly 15 to 20 minutes a month writing up notes. That puts the total time cost under an hour a week per person, small enough that it does not meaningfully compete with deliverable time, and it is a claim that can be checked against the actual calendar rather than taken on faith.
Trade-offs & pitfalls
The temptation is always to add more: formal training modules, a matching algorithm, quarterly surveys. A program with more infrastructure than the team has bandwidth to sustain decays within a few weeks. The senior distinction here is that a junior design assumes more structure is always better, while a senior deliberately underbuilds and only adds structure once a specific signal shows it is needed. A second pitfall is shared docs going stale because nobody owns freshness; assign light rotating ownership (whoever paired last updates the relevant page) rather than creating a separate docs-owner role, which is more overhead, not less. A third pitfall is picking the wrong rotation speed: too fast and no pair builds enough context to go deep; too slow and some people never get exposure to others. Match the cycle length to team size so everyone pairs with everyone within one cycle, as in the rotation above.
Implement an LRU cache in Swift with O(1) get(key:) and put(key:value:) operations. Describe the data structures you will use and provide Swift code showing the core logic (dictionary + doubly-linked list). Discuss thread-safety options for use from multiple threads and how to account for item costs such as image byte size.
Sample Answer
Approach (brief)
Use a Dictionary<Key, Node> for O(1) lookups and a doubly-linked list to maintain recency (head = most-recent, tail = least-recent). Each Node stores key, value, cost (e.g., image byte size). Track totalCost and evict from tail until under capacity.
Swift implementation (core logic)
final class LRUCache<Key: Hashable, Value> {
private class Node {
let key: Key
var value: Value
var cost: Int
var prev, next: Node?
init(key: Key, value: Value, cost: Int) {
self.key = key; self.value = value; self.cost = cost
}
}
private var map = [Key: Node]()
private var head: Node?
private var tail: Node?
private var totalCost = 0
private let capacityCost: Int
init(capacityCost: Int) { self.capacityCost = capacityCost }
private func moveToHead(_ node: Node) {
guard head !== node else { return }
// unlink
node.prev?.next = node.next
node.next?.prev = node.prev
if tail === node { tail = node.prev }
// insert at head
node.next = head
node.prev = nil
head?.prev = node
head = node
if tail == nil { tail = head }
}
private func insertAtHead(_ node: Node) {
node.next = head; node.prev = nil
head?.prev = node; head = node
if tail == nil { tail = head }
}
private func removeTail() -> Node? {
guard let node = tail else { return nil }
map[node.key] = nil
totalCost -= node.cost
if head === tail {
head = nil; tail = nil
} else {
tail = node.prev
tail?.next = nil
}
return node
}
func get(_ key: Key) -> Value? {
guard let node = map[key] else { return nil }
moveToHead(node)
return node.value
}
func put(_ key: Key, value: Value, cost: Int = 1) {
if let node = map[key] {
totalCost -= node.cost
node.value = value
node.cost = cost
totalCost += cost
moveToHead(node)
} else {
let node = Node(key: key, value: value, cost: cost)
map[key] = node
insertAtHead(node)
totalCost += cost
}
// Evict while over capacity
while totalCost > capacityCost {
_ = removeTail()
}
}
}
Complexity
- get / put: O(1) time; space O(n).
Thread-safety options
- For simple use, wrap public methods with a serial DispatchQueue (DispatchQueue(label:..., attributes: .concurrent) with .barrier for mutations) or use os_unfair_lock/NSLock for low-latency locking.
- Example: use private let queue = DispatchQueue(label: "lru.cache", attributes: .concurrent); read via queue.sync, writes via queue.async(flags: .barrier) to avoid races and keep reads concurrent.
Accounting for item costs (images)
- Provide cost parameter (bytes) per put; maintain totalCost; evict least-recent until totalCost <= capacityCost. For images, use image.pngData()?.count or image.costForCaching API; consider approximate costs to avoid expensive size computations.
Edge cases & notes
- Handle large single-item cost > capacity (optionally reject or allow by evicting all others).
- Consider weak references if values should be released when not strongly held elsewhere (NSCache alternative).
At the end of a meeting, how do you confirm next steps out loud in the room, and then again in a short written follow-up, so nothing gets lost between the conversation and the written record?
Sample Answer
Direct answer
State the decision and the immediate next steps out loud before the meeting ends, then send a short written follow-up within the hour that restates the same thing, so there's both an in-the-room confirmation and a durable record that matches it.
Structured elaboration
- Confirm verbally before people leave the room (or call). In the last minute or two, say "so to confirm, we've decided X, and the next steps are Y owned by Z by Thursday, does that match everyone's understanding?" This catches a misalignment while everyone who can correct it is still present.
- Watch for silence versus agreement. Nobody objecting isn't the same as everyone actively agreeing; a direct question ("does that match?") is more reliable than just pausing and moving on if no one immediately speaks up.
- Send the written follow-up promptly, ideally within the hour, restating the same decision and action items. The verbal confirmation and the written one should say the same thing; if they don't, that's usually a sign the verbal confirmation was rushed or unclear.
- Keep the written version short and scannable, matching the same content as the verbal confirmation rather than adding new information the room didn't actually agree to.
- Flag anything genuinely still unresolved, in both the verbal check and the written follow-up, rather than letting an unresolved point quietly look settled just because the meeting ended.
Worked example
Verbal, at the end of the meeting: "So to confirm: we're going with the phased rollout, Sam owns the migration plan by next Friday, and we're holding off on the customer announcement until that's done. Does that match what everyone heard?"
Written follow-up sent the same hour: "Recap from today: decided on the phased rollout. Sam: migration plan due next Friday. Customer announcement is on hold until the migration plan is ready. Shout if this doesn't match what you remember."
The two versions state the identical decision and owner, and the written version explicitly invites correction rather than assuming silence means agreement.
Trade-offs and pitfalls
- Skipping the verbal confirmation and only sending a written recap later means any misunderstanding surfaces after people have already left and possibly acted on their own interpretation.
- Skipping the written follow-up and only confirming verbally means anyone who wasn't in the room, or who forgets, has no record to check against.
- A written recap that silently adds detail beyond what was verbally confirmed can create a new source of disagreement; keep the two consistent, and if you realize something needs adding, flag it explicitly as new rather than folding it in unannounced.
Want to create your own tailored preparation guide using our deep research?
Get Started for FreeInterview-Ready Courses
Visual-first, interactive, structured learning paths
Browse Mobile Developer jobs
AI-enriched listings across hundreds of company career pages
Explore Jobs