Meta Mobile Developer (Junior Level) Interview Preparation Guide
Meta's interview process for Mobile Developers follows a comprehensive evaluation approach consisting of recruiter screening, technical phone screen, multiple onsite technical rounds, and behavioral assessment. For junior-level candidates, the process emphasizes coding fundamentals, mobile platform knowledge, and cultural fit. Expect 4-6 weeks from initial contact to final offer decision. The technical rounds focus on problem-solving ability, code quality, and understanding of mobile development challenges. Meta uses AI-enabled coding tools in some assessments and evaluates candidates on real-world thinking and communication alongside technical execution.
Interview Rounds
Recruiter Screening
What to Expect
Initial 30-minute conversation with Meta recruiter to assess basic qualifications, background, and career goals. This combines the initial recruiter phone screen and any follow-up recruiter calls. The recruiter will discuss your experience with mobile development, why you're interested in Meta, timeline, and explain the interview process. They will also verify your technical background and ensure you meet baseline requirements. At this stage, you'll receive a Metacareers account and access to preparation resources.
Tips & Advice
Be genuine and concise in your responses. Have a clear 2-3 minute 'Tell me about yourself' story prepared that highlights your mobile development experience. Show enthusiasm for Meta's mission and mobile products. Ask thoughtful questions about the role and team. Mention specific Meta products you use and appreciate. Be honest about your experience level as a junior developer—recruiters expect you to be early in your career. Confirm logistics (interview schedule, preparation resources, expected timeline). Create your Metacareers account immediately and review the email with preparation resources.
Focus Topics
Availability and Timeline Clarification
Confirm your availability for upcoming interview rounds, any potential scheduling constraints, and your expected start date if hired.
Practice Interview
Study Questions
Learning Ability and Growth Mindset
Share examples of how you've learned new technologies, solved difficult technical problems, and adapted to changing project requirements. Highlight curiosity and proactive learning.
Practice Interview
Study Questions
Mobile Development Skills Inventory
Clearly communicate your technical skills: which mobile platforms (iOS, Android), languages (Swift, Kotlin, Java, JavaScript), frameworks (React Native, Flutter), and tools you're proficient with. Be honest about your current level.
Practice Interview
Study Questions
Why Meta and Mobile Product Knowledge
Explain why Meta specifically appeals to you. Demonstrate familiarity with Meta's mobile applications (Facebook, Instagram, WhatsApp, Threads) and discuss what you find interesting about their technical or product approach.
Practice Interview
Study Questions
Professional Background and Mobile Development Journey
Articulate your path to mobile development, key projects you've worked on, technologies you've used (iOS/Android/React Native), and what excites you about mobile development as a career.
Practice Interview
Study Questions
Technical Phone Screen - Coding
What to Expect
45-60 minute technical coding interview conducted over video call via CoderPad or similar platform. You'll solve 1-2 coding problems with moderate complexity, typically involving data structures and algorithms relevant to mobile development scenarios. The interviewer observes your problem-solving approach, code quality, communication, and ability to handle feedback. This round assesses your core algorithmic thinking and coding fundamentals.
Tips & Advice
Start by clarifying the problem—ask clarifying questions even if the problem seems clear. Think out loud and explain your approach before coding. Write clean, readable code with proper variable names. Test your solution against the provided examples and edge cases. If stuck, communicate the difficulty and ask for hints rather than staying silent. Optimize for clarity first, then optimization. For mobile context, relate problems to real mobile scenarios when relevant (e.g., memory constraints, list rendering). Don't over-complicate; junior-level problems are designed to be solvable with fundamental techniques. If you use the CoderPad AI assistant, use it strategically—don't let it write entire solutions but let it help with syntax or refactoring.
Focus Topics
Hash Maps and Set-Based Problem Solving
Using hash maps and sets for efficient lookups, counting, duplicate detection, and multi-step problem solving. Understanding trade-offs between space and time complexity.
Practice Interview
Study Questions
Code Quality and Testing
Writing readable code with meaningful names, proper error handling, considering edge cases, and testing against examples. Ability to identify and fix bugs during execution.
Practice Interview
Study Questions
Linked Lists and Basic Tree Operations
Understanding linked list properties (cycle detection, reversal, merging), binary tree traversal (in-order, pre-order, post-order), tree properties (balance, height, lowest common ancestor).
Practice Interview
Study Questions
Problem-Solving Methodology and Communication
Ability to break down problems, articulate your approach, explain trade-offs, ask clarifying questions, and incorporate interviewer feedback. Communicating complexity analysis (time and space).
Practice Interview
Study Questions
Array and String Manipulation
Problems involving searching, sorting, filtering, and transforming arrays or strings. Includes finding subarrays with specific properties, detecting patterns, and manipulation of character data.
Practice Interview
Study Questions
Onsite Technical Round 1 - Mobile-Specific Coding
What to Expect
45-60 minute in-depth coding interview focused on mobile platform-specific concepts. This round may present problems that require knowledge of iOS/Android development or discuss how to implement features on mobile. Problems may involve mobile-specific considerations like performance constraints, device diversity, or common mobile development patterns. You'll be expected to think about real mobile app implementation challenges while solving algorithmic problems.
Tips & Advice
Demonstrate familiarity with the mobile platform (iOS or Android) you've worked with most. If a problem relates to mobile concepts (e.g., managing UI state, handling async operations), connect your solution to mobile-specific patterns. Be ready to discuss how your solution would perform on real devices with limited memory or battery. For junior level, interviewers expect you to know fundamentals of your chosen platform but won't expect expert knowledge of every framework. If given a choice of language, use what you're most comfortable with. Be clear about trade-offs: sometimes an optimal algorithmic solution isn't practical for mobile due to memory constraints. Discuss testing considerations for mobile (different screen sizes, OS versions). Ask questions about the mobile context if it's not clear from the problem statement.
Focus Topics
Cross-Platform Framework Knowledge (React Native or Flutter if applicable)
If your experience includes React Native or Flutter: understanding component lifecycle, state management, platform-specific code, performance considerations, and debugging approaches.
Practice Interview
Study Questions
Mobile Performance and Optimization
Understanding memory constraints on mobile devices, efficient list rendering (RecyclerView/UITableView), battery optimization, reducing network calls, image optimization, and profiling tools.
Practice Interview
Study Questions
Algorithm Implementation with Mobile Context
Solving coding problems while considering mobile constraints—optimizing for memory usage, battery, and network bandwidth. Adapting solutions based on device capabilities.
Practice Interview
Study Questions
iOS Development Fundamentals (if iOS focus)
Swift syntax, iOS app lifecycle, view controllers, delegates, view hierarchies, memory management, common design patterns (MVC, MVVM). Understanding UIKit or SwiftUI basics.
Practice Interview
Study Questions
Asynchronous Programming and State Management
Handling async operations in mobile (networking, database queries), managing UI state, preventing memory leaks, understanding threading models, and using appropriate tools (AsyncTask, coroutines, RxJava, reactive frameworks).
Practice Interview
Study Questions
Android Development Fundamentals (if Android focus)
Kotlin/Java syntax, Android activity and fragment lifecycle, view layouts, intent system, async programming with coroutines, common architectural patterns (MVVM, MVI). Understanding Material Design.
Practice Interview
Study Questions
Onsite Technical Round 2 - Standard Coding or AI-Enabled
What to Expect
45-60 minute coding interview following Meta's standard format or the newer AI-enabled format. If AI-enabled, you'll work through one thematic multi-part problem (understand existing code, implement new functionality, extend the system). You have access to AI assistance in CoderPad but aren't required to use it. If standard coding, expect 1-2 conventional algorithmic problems. Both formats emphasize your ability to think critically, write quality code, and communicate effectively.
Tips & Advice
If this is the AI-enabled round: treat the AI as a helpful tool, not a solver. Use it for scaffolding or refactoring, but understand every line you submit. Read error messages carefully and explain what you're fixing. For the multi-part structure: Part 1 focuses on understanding existing code—show you can read and comprehend code that you didn't write. Part 2 is implementing clear requirements—this is where you show competence. Part 3 is extending/improving—show architectural thinking. If standard coding: same approach as Round 2 but potentially slightly harder problems. For junior level, expect questions at the level where you can solve them with time and thought, not instant solutions. Communicate your reasoning throughout. Show your verification process—run test cases, check edge cases, explain why your solution works.
Focus Topics
Effective Use of Tools and AI Assistance
If applicable, leveraging AI tools strategically without becoming dependent on them. Understanding when to use AI scaffolding vs. writing code yourself. Knowing how to prompt effectively.
Practice Interview
Study Questions
System Extension and Refactoring
Extending existing systems with new functionality, improving code quality, maintaining consistency with existing architecture, and considering future extensibility.
Practice Interview
Study Questions
Debugging and Problem-Solving Under Pressure
Finding and fixing errors efficiently, reading test failures, understanding root causes, and iterating toward solutions when initial approaches don't work.
Practice Interview
Study Questions
Code Reading and Understanding
Ability to navigate unfamiliar codebases, understand existing implementations, identify bugs, and build on working code. This is critical in real work where you inherit and extend existing systems.
Practice Interview
Study Questions
Feature Implementation and Requirement Translation
Taking clear requirements and implementing them correctly. Breaking down requirements into steps. Testing implementation against requirements. Handling user stories and specification-driven development.
Practice Interview
Study Questions
Onsite Technical Round 3 - Product Design or Mobile Architecture
What to Expect
45-minute discussion-based technical round focused on product design or mobile architecture at a component/feature level. For junior level, this is lighter than full system design interviews. You may be asked to design a feature in a Meta product, architecture for a mobile application, or discuss how you'd approach a mobile problem. The round emphasizes practical thinking about mobile constraints, user experience, and engineering trade-offs. Expect open-ended questions where you guide the conversation.
Tips & Advice
This is not a complex distributed systems design round—focus on component-level and feature-level thinking appropriate for junior level. Start by clarifying requirements and constraints. Make reasonable assumptions about scale (e.g., millions of users, but you're not designing planet-scale infrastructure). Draw diagrams if possible (on a whiteboard or CoderPad). Discuss trade-offs: performance vs. complexity, user experience vs. engineering effort, speed vs. completeness. For mobile specifically: consider device constraints, offline support, sync strategies, battery efficiency, and network reliability. Reference Meta's actual products when relevant—what design choices does Instagram make? Why might WhatsApp use end-to-end encryption? Show understanding of real mobile challenges. Ask clarifying questions: What's the target audience? What are key features? What devices/platforms? Be realistic for junior level—you're not expected to architect Netflix-scale systems, but you should think clearly about feature design.
Focus Topics
Real Product Knowledge - Meta's Mobile Apps
Understanding design choices and constraints in Meta products (Facebook, Instagram, WhatsApp, Threads). Being able to discuss why certain features are designed certain ways and what mobile challenges they solve.
Practice Interview
Study Questions
Mobile Performance and Trade-off Analysis
Understanding performance implications of design decisions. When to cache, when to lazy-load, balancing feature richness with app size, considering memory and battery implications.
Practice Interview
Study Questions
State Management Architecture
Approaches to managing application state in mobile apps. When to use local storage, in-memory state, or server-sourced state. Handling complex state transitions and preventing UI inconsistencies.
Practice Interview
Study Questions
Data Synchronization and Offline Support
Strategies for syncing data between client and server, handling offline scenarios, conflict resolution, and keeping users' experiences smooth despite network issues.
Practice Interview
Study Questions
Mobile API Design and Integration Patterns
Designing or working with APIs for mobile consumption. Considering mobile constraints: minimize data transfer, optimize for latency, batch operations. Understanding pagination, filtering, and caching strategies.
Practice Interview
Study Questions
Mobile Feature Architecture and Design
Designing features from scratch at the mobile app level. Thinking through UI/UX, data flow, state management, and API requirements. Breaking down complex features into implementable components.
Practice Interview
Study Questions
Onsite Behavioral and Cultural Fit Round
What to Expect
45-60 minute behavioral interview focused on your values, teamwork, problem-solving approach, and alignment with Meta's culture. The interviewer will ask about past experiences, how you handle challenges, conflicts, ambiguity, and feedback. This round evaluates Meta's core values: Impact, Speed, Focus, Integrity, Community. For junior level, expect discussion of learning experiences, teamwork, and how you've grown technically. The interviewer is assessing whether you'll thrive in Meta's fast-paced, engineering-driven culture and whether you fit the team.
Tips & Advice
Prepare 4-5 concrete stories from your work experience covering: overcoming a technical challenge, collaborating with teammates, receiving critical feedback and responding to it, working in ambiguity, and a project you're proud of. Use the STAR method (Situation, Task, Action, Result) but keep stories concise (2-3 minutes). Align your stories with Meta's values—show Impact (what did your work enable?), Speed (how did you move fast?), Focus (what did you prioritize?), Integrity (what's an example of doing the right thing?), Community (how did you help your team?). For junior level, don't overstate your impact—be humble but confident. Talk about what you learned from challenges. Show growth mindset: how did you improve? How did you handle not knowing something? Ask questions that show you understand Meta's mission and culture. Be authentic—interviewers can tell when you're faking alignment. Prepare questions about the team, role expectations, and how to succeed at Meta.
Focus Topics
Handling Ambiguity and Change
Examples of working with unclear requirements, changing priorities, or ambiguous problems. How you bring structure to chaos and stay productive despite uncertainty.
Practice Interview
Study Questions
Technical Problem-Solving and Ownership
Examples of taking ownership of problems, debugging issues, pushing through challenges, and delivering solutions. Showing persistence and problem-solving mindset.
Practice Interview
Study Questions
Teamwork and Collaboration
Examples of effective collaboration, cross-functional work, helping teammates, and building positive team relationships. How you communicate and resolve disagreements constructively.
Practice Interview
Study Questions
Learning Ability and Growth Mindset
Demonstrating openness to feedback, examples of learning new technologies, adapting to challenges, and growing from mistakes. Showing curiosity and proactive learning.
Practice Interview
Study Questions
Meta Core Values Alignment
Understanding and demonstrating Meta's core values (Impact, Speed, Focus, Integrity, Community) through concrete examples from your experience. Showing how your approach to work aligns with Meta's culture.
Practice Interview
Study Questions
Frequently Asked Mobile Developer Interview Questions
Users report your Android app freezes and shows an 'App Not Responding' dialog, but only for some of them. How would you find what's blocking the main thread?
Sample Answer
Direct answer
Android flags an ANR when the main thread ignores input for about 5 seconds. Capture a Perfetto/systrace around the freeze and look for one long block on the main thread track, or pull the stack Play Console or Crashlytics already captured for users you can't reproduce on.
Structured elaboration
- Reproduce under Android Studio's Profiler if possible, or pull the ANR trace from Play Console's Android vitals for wild reports.
- Look for a synchronous disk write (
SharedPreferences.commit()), a synchronous network call, or a slowonDrawoccupying the main thread for the full gap. - Cross-reference affected devices/OS versions; concentration on low-end hardware suggests CPU-bound work that's just too slow there.
- Add
StrictModedisk and network detectors in debug builds to catch this before it ships.
Worked example
The ANR threshold is 5,000 ms:
6,200 ms (blocked)>5,000 ms (threshold)
A single commit() blocking 6,200 ms is sufficient on its own to trigger the ANR.
Trade-offs and pitfalls
Swapping to apply() fixes the freeze but slightly weakens the durability guarantee, a queued write instead of a confirmed one, usually the right trade for responsiveness.
What the interviewer probes next
How StrictMode would have caught this, and how you'd choose between apply(), a coroutine, or WorkManager.
How do you keep track of the decisions made during a cross-functional project so the reasoning behind them doesn't get lost or re-litigated later?
Sample Answer
Direct answer
Keep a single, easy-to-find decision log tied directly to the work it affects: what was decided, the options considered, the reasoning, and who owns it, updated by whoever is making the decision at the moment it is made, not reconstructed later from memory.
Structured elaboration
What belongs in an entry
A short, consistent structure works better than a long one, because people will actually fill it out: a title, the date, who owns it, the context in one or two sentences, the options considered with their trade-offs, the decision itself, and the reasoning behind it in a few bullet points.
Where it lives
The log needs to be one discoverable place, linked from the tickets, docs, or roadmap items it affects, not scattered across meeting notes and chat threads. A shared doc or wiki page with a simple table works; the tool matters less than the discipline of always linking to it.
Who keeps it current
The person who owns the decision, not a rotating scribe with no stake in it, writes or finalizes the entry, ideally right after the decision is made, while the reasoning is still fresh and easy to state accurately.
How it gets used afterward
In retrospectives, revisit decisions that affected the outcome and check whether the original assumptions held. For onboarding, a short list of the most consequential recent decisions gives a new team member the context that would otherwise take weeks of osmosis to pick up.
Worked example
A team is deciding between two ways to notify users of an event: a push notification versus an in-app banner. The entry, once decided, looks like this: title, "Notification channel for event alerts"; date and owner, the decision owner's name and the date; context, users were missing time-sensitive alerts under the current in-app-only approach; options considered, push notification (faster delivery, requires a new permission prompt), in-app banner only (no new permission needed, slower to be seen), and both channels (best coverage, more engineering and support surface); decision, push notification with an in-app banner as a fallback for users who decline the permission; reasoning, the delay in the in-app-only approach was the specific problem being solved, and the fallback covers users who opt out.
Anyone who later asks why the team does not just use an in-app banner, since it is simpler, can read this entry and see the trade-off was already considered, rather than re-litigating it from scratch.
Trade-offs and pitfalls
A log nobody updates is worse than no log: it creates false confidence that the reasoning is captured somewhere, while actually going stale. The fix is keeping entries short enough that updating one takes minutes, rather than requiring a formal write-up every time.
A log can also be used as a weapon later, such as insisting a past decision still holds in a situation where circumstances genuinely changed and revisiting was the right call. The log should record reasoning, not lock in a decision forever; a review date or a note on when to re-evaluate keeps it a living reference instead of a trap.
Design an offline-first sync strategy for a mobile notes app that supports create/edit/delete while offline, background sync when connectivity returns, and deterministic conflict resolution. Describe data model on client, change tracking, server API endpoints, sync algorithm (push/pull), and conflict resolution approaches (LWW, operational transforms, CRDTs).
Sample Answer
Clarify goals & constraints
Offline-first mobile notes app must allow create/edit/delete while offline, minimize data/battery, guarantee deterministic conflict resolution, and resume sync in background when connectivity returns.
Client data model
- Note: { id: UUID, title, body, updatedAt: ISO, deleted: bool, tombstoneAt?: ISO, version: int/lamport }
- Local metadata: { lastSyncedAt, pendingOps: [] }
- Persist in SQLite/Realm for durability.
Change tracking
- Append-only local operation log (pendingOps): { opId: UUID, noteId, type: create/update/delete, payload, clientTs, lamport }
- Keep tombstones for deletes until server ack.
Server API endpoints
- POST /sync/push — accept batch ops, return per-op ack + server version
- GET /sync/pull?since=timestamp — return all server-side changes since timestamp
- POST /notes/resolve — optional for manual conflict resolution audit
Sync algorithm (push/pull)
- Background job/network callback triggers sync.
- Push: send batched pendingOps (limited size), retry with exponential backoff, optimistic UI.
- Server applies ops (idempotent using opId), updates server version, responds with applied list.
- Pull: request changes since lastSyncedAt, merge into local DB, mark pendingOps as synced.
- Resolve conflicts deterministically on client using agreed policy.
Conflict resolution approaches
- LWW (last-writer-wins): easy (use lamport or serverTs). Loses intermediate edits.
- Operational Transforms: good for rich collaborative editing; complex, needs server OT engine.
- CRDTs (recommended for notes text/blocks): converge without central ordering; use sequence CRDT (RGA/Yjs) for body, tombstone-aware for deletes. Combine with per-field LWW for metadata.
- Practical hybrid: use CRDT for body text, LWW (lamport) for title/metadata, tombstones for deletes.
Mobile specifics & best practices
- Use WorkManager (Android) / BackgroundTasks (iOS) for reliable background sync.
- Compress batches, limit frequency, respect battery/data settings.
- Provide UI conflict indicators and manual merge when automatic resolution discards content.
Design an algorithm to find the first non-repeated word in a very large text file that cannot be fully loaded into memory. Discuss streaming approaches, two-pass algorithms, external sort, or probabilistic methods. Provide a solution that balances memory with accuracy and explain complexity.
Sample Answer
Direct answer
The clean solution is a two-pass, hash-map-based streaming algorithm: pass one builds a hash map
of word to frequency count without holding the file in memory; pass two re-scans the file in
original order and returns the first word whose count is exactly 1. Peak memory is bounded by
the number of DISTINCT words, not the file size, which is the property that makes this work when
the file itself does not fit in RAM. External sort and probabilistic counting are the fallbacks
when even the distinct-word count is too large to hold in memory.
Structured elaboration
Two-pass streaming (the default answer). Read the file once, line by line (or in fixed-size
chunks), splitting into words and incrementing a hash map word -> count. This pass never holds
more than one chunk plus the running map in memory. Read the file a SECOND time, in the same
original order, and return the first word whose final count (from pass one) equals 1. This
requires the file to be re-readable (a normal disk file, or a cursor that can seek back to the
start), which is the usual case for "a file too large for memory" rather than a live network
stream.
Why two passes and not one. A single pass cannot know a word's FINAL count while still
partway through the file, so a naive "return the first word not seen before" is wrong: that word
might repeat later. You need the complete counts from a first pass before you can trust any
word's non-repeating status during a second, order-preserving pass.
When the hash map itself does not fit (extremely large vocabulary). Fall back to external
sort: sort the words (using external merge sort, since the word list itself may not fit in
memory) so identical words become adjacent, then a linear scan of the sorted output counts each
run of identical words. This trades hashing's expected O(n) for sorting's O(n log n), but
removes the requirement that the distinct-word count fit in memory, since sorted runs can be
merged from disk. Recovering "first non-repeated IN ORIGINAL ORDER" from a sorted pass requires
carrying along original position as a secondary sort key, then a small final step to pick the
minimum position among words with count 1.
When exactness itself is negotiable. A Count-Min Sketch gives an approximate frequency count
in sublinear memory in one pass, at the cost of occasionally over-counting a word (never
under-counting), which could cause a truly non-repeated word to be misreported as repeated. Use
this only when a small, disclosed false-negative rate ("might miss the true answer") is an
acceptable trade against a hard memory ceiling that even a hash map of distinct words cannot
meet.
Complexity. Two-pass hashing: O(n) time for n total words across both passes, O(d)
space for d distinct words. External sort: O(n log n) time, O(chunk size) space per merge
step. Probabilistic (CMS): O(n) time, O(sketch size) space, independent of d, with a
disclosed error rate instead of exactness.
Worked example
import io
def first_non_repeated_word_two_pass(file_like):
counts = {}
file_like.seek(0)
for line in file_like:
for word in line.split():
counts[word] = counts.get(word, 0) + 1
file_like.seek(0)
for line in file_like:
for word in line.split():
if counts[word] == 1:
return word
return None
text = "the quick brown fox jumps over the lazy dog the fox runs"
print(first_non_repeated_word_two_pass(io.StringIO(text)))
text2 = "a b a b c"
print(first_non_repeated_word_two_pass(io.StringIO(text2)))
def broken_single_pass_first_sight(text_str):
seen = set()
for w in text_str.split():
if w not in seen:
return w
seen.add(w)
return None
print(broken_single_pass_first_sight(text2))
Running this: against "the quick brown fox jumps over the lazy dog the fox runs", it prints
quick. Manually counting confirms why: the appears 3 times, fox appears 2 times, and every
other word (quick, brown, jumps, over, lazy, dog, runs) appears once; scanning in
original order, quick is the first word whose count is 1. Against the harder case
"a b a b c", the two-pass function prints c, while the naive single-pass "first word not seen
before" check prints a, which is wrong: a looks unrepeated the first time it is seen, and only
a completed count (available after pass one) reveals that a repeats later, exactly the bug
two-pass counting avoids.
Trade-offs and pitfalls
- The most common mistake is trying to answer this in a single pass by returning the first
word not yet in a "seen" set. That is provably wrong whenever a word's later repeat has not
been read yet, as shown above with"a b a b c". - External sort adds
log nfactor but removes the hash-map memory requirement; prefer it
when the number of DISTINCT words is itself too large for memory, not just the raw file size. - A Count-Min Sketch trades exactness for a fixed, disclosed error budget; only acceptable
when the interviewer's stated constraint is a hard memory ceiling that no exact structure can
meet, and only with the error trade-off stated explicitly, not silently assumed. - Re-readability matters: this two-pass approach assumes the file can be scanned a second
time from the start. If the input is a genuinely single-pass stream (no seek), you cannot do
this exactly at all and must accept probabilistic counting or reduce the guarantee to
"first non-repeated among the lastWwords" instead.
Design an offline-first mobile messaging system supporting multi-device users. Cover ordering guarantees, delivery acknowledgements, deduplication, message edits/deletes, offline message composition and replay, push coordination, and end-to-end encryption. Describe client responsibilities, server responsibilities, sync protocol (how operations are represented), and scalability considerations.
Sample Answer
Situation & goals (brief)
Design an offline-first mobile messaging system for multi-device users that is responsive offline, provides causal ordering where possible, reliable delivery/acknowledgement, deduplication, edits/deletes, push coordination, replay, and E2EE.
Client responsibilities
- Generate locally-unique operation IDs (UUID + device monotonic counter + timestamp) and keep a per-device logical clock.
- Support offline composition: store ops in local persistent queue; show optimistic state in UI.
- Apply CRDT/operation-log model for messages (send, edit, delete) and maintain causal metadata (parent refs).
- Retry sending when online; handle ACKs and NACKs; replay unapplied remote ops.
- Verify and decrypt messages with device keys; hold per-device key store + session management (e.g., X3DH/Double Ratchet).
- Deduplicate by op ID; merge edits using last-writer-wins by logical clock plus tie-breaker (device ID).
Server responsibilities
- Stateless or minimally stateful message router and storage of encrypted op-log per conversation.
- Provide per-user per-device sync tokens (vector clocks / op counters) and push fanout coordination (APNs/FCM) for offline devices.
- Persistent durable store (append-only log) for ops to allow replay; index for unread and per-device delivery state.
- Perform permission checks and rate limiting; do not decrypt payload.
Sync protocol / operation representation
- Operations as signed, encrypted JSON objects:
text
{ op_id, op_type: send|edit|delete|ack, parent_op_ids[], sender_device_id, sender_user_id, counter, timestamp, payload_ciphertext, signature }- Client sends ops; server appends and responds with commit ACK and sequence number.
- Client requests delta since sync token (vector of per-device counters) and applies ops in causal order (respect parent refs).
- Delivery ACK ops let server mark per-device delivery; server can garbage-collect when all devices ACK.
Ordering guarantees & conflict resolution
- Preserve causal order via parent refs and deliver ops in topological order. For independent concurrent sends, show by server sequence or timestamp with stable tie-breaker. Edits/deletes resolved by last-writer-wins on (counter, device_id) or application-level merge for rich content.
Delivery acknowledgements & deduplication
- Per-device delivery ACK ops recorded on server; read receipts similarly. Clients dedupe by op_id; server dedupes before appending.
Message edits/deletes & replay
- Edit/Delete are ops referencing target message op_id. Clients apply ops in order; if target missing, buffer until original arrives. Server stores ops and serves replay to new devices or after reinstalls using op-log since zero or snapshot.
Push coordination
- Server sends minimal push notifications (metadata only) to devices via APNs/FCM; payload-less pushes trigger encrypted delta sync. For active app instances, server pushes via persistent socket (WebSocket/QUIC) with batching to reduce wakeups.
End-to-end encryption
- Use per-device identity keys + session keys (X3DH for initial, Double Ratchet for forward secrecy). Encrypt message payload; sign op metadata. Server never has plaintext. Key distribution handled via signed device list synced across devices; support key rotation and key backup (encrypted with user passphrase).
Scalability considerations
- Shard op-log by conversation ID; use append-only logs with immutable segments and compact snapshots for long histories. Use per-conversation fanout workers for push. Use eventual-consistent state for reads; tombstone GC after all devices ACK. Cache hot conversations in in-memory stores; use rate limiting and backpressure to protect mobile clients.
Trade-offs
- CRDT/operation-log favors offline UX and deterministic merges at cost of metadata size. Strong total ordering needs central sequencer (higher latency). E2EE prevents server-side search/indexing; consider encrypted indexes if necessary.
This design balances offline-first mobile UX, multi-device consistency, and end-to-end security while keeping server responsibilities minimal and scalable.
Your team owns a large iOS and Android app built on callbacks, GCD and thread pools. How would you migrate it incrementally to structured concurrency (Swift async/await, Kotlin coroutines) without regressions?
Sample Answer
Direct answer
Migrate from the edges inward, one module at a time, behind a safety net, and keep old and new APIs side by side until the numbers prove parity. The order: (0) build the safety net, (1) wrap leaf callback APIs, (2) turn queue-guarded state into actors (Swift) or confined coroutines (Kotlin), (3) switch callers feature by feature with shims for old callers, (4) retire the old paths. Do not rewrite call graphs in a big bang, and do not translate DispatchQueue code line by line, because the semantics differ in four ways listed below.
Terms
Structured concurrency means each task has a parent scope that cancels it and waits for it, so no work is orphaned. In Swift that is async/await, async let, task groups and Task cancellation. In Kotlin it is coroutines with a CoroutineScope and Job. GCD (Grand Central Dispatch) is iOS's queue-based thread pool library. An actor is a Swift type that lets one caller at a time touch its state. A continuation is a handle that lets callback-style code resume a suspended async function: you call resume exactly once, and the Checked variant detects misuse instead of failing silently: a second resume traps and a missing resume logs a warning. A cooperative pool is the small fixed set of threads (about one per CPU core) that runs all async tasks; tasks give the thread back whenever they await, which is why blocking one is harmful.
Phase 0: safety net before any change
- Baseline numbers for each flow you will touch: p50/p95 latency, crash-free rate, hang/ANR rate (an ANR is Android's Application Not Responding dialog; Android's own documentation lists a deadlock between the main thread and another thread, and slow I/O on the main thread, among the common causes).
- Characterisation tests: tests that record what the code does today (ordering, error cases, what happens on cancel), including the odd behaviour, rather than what it should do, so a migration that changes behaviour fails a test. The test in the run below does this for a cache.
- Run Thread Sanitizer on iOS (a compiler-instrumented tool that reports data races as they happen) and StrictMode on Android (a debug-time tool that flags disk or network work on the main thread) in CI. On Swift, turn on stricter compile-time checks gradually:
-strict-concurrency=minimal(checks only code that has already opted in to concurrency annotations), thentargeted(also checks code that usesSendableand actors), thencomplete(checks everything and reports every unsafe crossing of a thread boundary, the same checks Swift 6 mode turns into errors), and only then the Swift 6 language mode. The most useful signal is the warning count atcomplete; getting it to zero is what makes the language-mode switch safe. All three values of the flag were accepted by the Swift 6.0.3 compiler on the Step 1 to 3 program below, which compiled with no diagnostics at each level. - A kill switch per migrated module (a remotely controlled flag that switches back to the old code path), so a regression is a flag flip, not a release.
The four semantic differences that cause regressions
- Ordering. A serial queue runs submitted blocks in order. A
Task { }per event does not guarantee order (before:queue.async { log("A") }; queue.async { log("B") }always logs A then B; after:Task { log("A") }; Task { log("B") }may log B first), and an actor can interleave two calls whenever one of them hits anawait(reentrancy: while call 1 is suspended at anawaitinside the actor, call 2 may run and change the state call 1 read before suspending, so call 1 may resume with a stale assumption such as "the key is not in the cache yet"). For ordered event streams use anAsyncStreamconsumed by one task (Swift) or aChannel/Flowwith one collector (Kotlin). - Blocking. Async code runs on a small cooperative pool, about one thread per CPU core. Blocking one of those threads (
DispatchSemaphore.wait,sync) can starve the pool. The run below shows it: with 14 cores, 13 tasks blocked on a semaphore still let the releasing task run, but 14 blocked tasks did not finish within the 2 second limit (the demo then releases them by hand so the next trial starts clean). With 13 blocked, one thread was still free to run the task that signals the semaphore; with 14, every pool thread was parked waiting for a signal that only a task needing a pool thread could send, so without the manual release they would wait forever. On Android the same rule is the main-thread rule: blocking I/O goes toDispatchers.IOthroughwithContext, andrunBlockingon the main thread invites an ANR. - Lifetime and cancellation. Callback code often kept running after the screen closed. Structured tasks are cancelled with their scope, so side effects you relied on (an analytics event, a final write) may silently stop. Audit each flow: either move the must-run work to a scope that outlives the screen, or make it non-cancellable on purpose. In Kotlin never swallow
CancellationExceptionin a broadcatch. - Which thread delivers the result. Callbacks that were documented to arrive on the main thread must become
@MainActor(Swift) orDispatchers.Main(Kotlin) explicitly. Do not leave it implicit.
Phase 1 to 3 in code
The Swift below was compiled with Swift 6.0.3 in a swift:6.0 Linux container (aarch64), in both the Swift 5 and the Swift 6 language mode, and run. It wraps one callback API with a continuation (a single resume), keeps old callers working through a shim over the new function (via modern: Bool selects whether the shim forwards to the old implementation or to the new async one; as Profile tells the compiler which of the two same-named fetchProfile overloads, callback or async, to call), and replaces a serial-queue-guarded cache by an actor, with a test that both implementations agree after 1,000 concurrent writes.
import Foundation
// ---------- Step 1: wrap one callback API as async, exactly one resume ----------
struct Profile: Sendable, Equatable { let id: Int; let name: String }
/// Legacy API: completion handler, called once, on an arbitrary queue.
func fetchProfile(id: Int, completion: @escaping @Sendable (Result<Profile, Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.005) {
id < 0 ? completion(.failure(URLError(.badURL)))
: completion(.success(Profile(id: id, name: "user\(id)")))
}
}
func fetchProfile(id: Int) async throws -> Profile {
try await withCheckedThrowingContinuation { cont in
fetchProfile(id: id) { cont.resume(with: $0) }
}
}
// ---------- Step 2: keep old callers compiling with a shim over the new API ----------
// The legacy API called back on an arbitrary queue, and so does this shim:
// a caller that needs the main thread must hop there itself (see difference 4).
func fetchProfile(id: Int, completion: @escaping @Sendable (Result<Profile, Error>) -> Void,
via modern: Bool) {
guard modern else { return fetchProfile(id: id, completion: completion) }
Task {
do { completion(.success(try await fetchProfile(id: id) as Profile)) }
catch { completion(.failure(error)) }
}
}
// ---------- Step 3: replace a lock/serial-queue guarded cache with an actor ----------
final class QueueCache: @unchecked Sendable { // before
private let queue = DispatchQueue(label: "cache")
private var items: [Int: Profile] = [:]
func put(_ p: Profile) { queue.sync { items[p.id] = p } }
func get(_ id: Int) -> Profile? { queue.sync { items[id] } }
var count: Int { queue.sync { items.count } }
}
actor ActorCache { // after
private var items: [Int: Profile] = [:]
func put(_ p: Profile) { items[p.id] = p }
func get(_ id: Int) -> Profile? { items[id] }
var count: Int { items.count }
}
// ---------- Characterisation test: both versions must behave the same ----------
let old = QueueCache()
let new = ActorCache()
await withTaskGroup(of: Void.self) { g in
for i in 0..<1000 {
g.addTask { let p = Profile(id: i % 100, name: "n\(i % 100)"); old.put(p); await new.put(p) }
}
}
var same = true
for i in 0..<100 { if old.get(i) != (await new.get(i)) { same = false } }
print("caches agree on all 100 keys:", same, "| counts:", old.count, await new.count)
// Wrapper behaves like the original, for success and for failure.
print("async success:", try await fetchProfile(id: 7))
do { _ = try await fetchProfile(id: -1) as Profile } catch { print("async failure:", (error as? URLError)?.code == .badURL) }
let viaShim: Profile = try await withCheckedThrowingContinuation { cont in
fetchProfile(id: 3, completion: { cont.resume(with: $0) }, via: true)
}
print("shim over async:", viaShim)
It printed:
caches agree on all 100 keys: true | counts: 100 100
async success: Profile(id: 7, name: "user7")
async failure: true
shim over async: Profile(id: 3, name: "user3")
The blocking-starvation check for item 2 above, compiled and run the same way:
import Foundation
let cores = ProcessInfo.processInfo.activeProcessorCount
print("cores:", cores)
/// `blocked` tasks each park their thread on a semaphore; one more task would release them all.
/// Returns true if everything finished within 2 seconds.
func finishesInTime(blocked: Int) -> Bool {
let gate = DispatchSemaphore(value: 0)
let done = DispatchSemaphore(value: 0)
Task {
await withTaskGroup(of: Void.self) { group in
for _ in 0..<blocked { group.addTask { gate.wait() } } // blocks a cooperative thread
group.addTask { for _ in 0..<blocked { gate.signal() } } // needs a free thread to run
}
done.signal()
}
let ok = done.wait(timeout: .now() + 2) == .success
if !ok { for _ in 0..<blocked { gate.signal() } } // release them so the next trial starts clean
return ok
}
print("blocked tasks = cores - 1, finished in 2 s:", finishesInTime(blocked: max(cores - 1, 1)))
print("blocked tasks = cores, finished in 2 s:", finishesInTime(blocked: cores))
It printed, in a container with 14 cores:
cores: 14
blocked tasks = cores - 1, finished in 2 s: true
blocked tasks = cores, finished in 2 s: false
Pool size depends on how many CPUs the process can actually use, but the rule holds on any of them: never block inside async code. The cores - 1 threshold above holds only where the reported core count matches the pool width. In a container restricted to a subset of CPUs (a CPU affinity mask), activeProcessorCount still reported 14 while the pool was evidently narrower, and then even cores - 1 blocked tasks did not finish. Size such an experiment from the CPUs the process may run on, not from the reported count.
On Android, the equivalent first step:
// Same idea as the Swift wrapper: one callback API, one suspend function.
suspend fun fetchProfile(id: Int): Profile = suspendCancellableCoroutine { cont -> // suspends the coroutine until cont is resumed
val call = legacyClient.fetch(id, object : Callback {
override fun onSuccess(p: Profile) { cont.resume(p) }
override fun onError(e: Throwable) { cont.resumeWithException(e) }
})
cont.invokeOnCancellation { call.cancel() } // runs if the coroutine is cancelled before the callback fires: cancel the legacy call too
}
class ProfileViewModel(private val io: CoroutineDispatcher = Dispatchers.IO) : ViewModel() {
fun load(id: Int) = viewModelScope.launch { // cancelled when the ViewModel is cleared
val p = withContext(io) { repository.blockingLoad(id) } // blocking work stays on IO
_state.value = UiState.Loaded(p)
}
}
Android's coroutine guidance says to inject dispatchers rather than hardcode them (so tests can replace them), to make suspend functions safe to call from the main thread by moving blocking work with withContext, to start coroutines in the ViewModel, and to avoid GlobalScope.
Rollout order and gates
Pick the module with the least shared state and the best test coverage first (a networking leaf, not the sync engine). Ship it dark behind the flag, ramp 1%, 10%, 50%, 100%, comparing the Phase 0 metrics at each step, and hold a stage until the new path is no worse on crash-free rate and hang/ANR rate. Migrate the two platforms in step so behaviour (cancel, retry, ordering) stays consistent for users who use both. Check the minimum OS version your Xcode and Swift versions support for async/await against your deployment target; below it, keep the callback API as the public surface.
What would change the plan: if the app has a large, well-tested serial-queue core that is not a source of bugs, leave it and only wrap it. Migrate what hurts (leaks, races, callback pyramids), not everything.
Design an API and server-side protocol for resumable uploads supporting 10k concurrent clients, files up to 200MB, and intermittent mobile connectivity. Specify endpoints for initiating an upload, uploading chunks, resuming, validating integrity, and finalizing. Explain what server state you would persist, how idempotency applies at each step, and how you would handle abandoned uploads and abuse.
Sample Answer
Direct answer
Model the upload as a stateful resource, an upload session with its own id, and make idempotency work at two different levels: session creation is protected by a client-supplied Idempotency-Key so a retried "start upload" call cannot spin up two competing sessions for the same file, and each chunk is naturally idempotent because it is addressed by its byte range plus a checksum, so re-sending the same range is a safe no-op rather than a duplicate write. Everything the server needs to persist is small: the session's metadata and the set of byte ranges already received; the file bytes themselves live in object storage, not in the session record.
Structured elaboration
Endpoints:
POST /uploads(initiate). Body:filename,total_size,content_type. Headers:Idempotency-Key(recommended). Response201:{ upload_id, expires_at, chunk_size_hint }.PATCH /uploads/{upload_id}(upload a chunk). Headers:Content-Range: bytes {start}-{end}/{total}, a chunk checksum header. Response200:{ received_ranges: [...], next_offset }. Accepts chunks out of order.GET /uploads/{upload_id}(resume). Returnsreceived_rangesandnext_offsetso a reconnecting mobile client knows exactly what is missing.POST /uploads/{upload_id}/complete(finalize). Body: client-computed whole-file checksum. Server assembles/validates and returns the final file id.DELETE /uploads/{upload_id}(abandon explicitly).
Server state persisted:
- Session row:
upload_id,user_id,filename,total_size,content_type,state(initiated / in_progress / completed / aborted),created_at,last_activity_at,expires_at. - Received-ranges record: a compact list of
{start, end, checksum}entries per chunk actually stored, not the chunk bytes themselves (those go straight to object storage). - Nothing here requires holding an open connection between chunks; a mobile client can drop off the network for minutes and resume against the same
upload_id.
Idempotency at each step:
- Initiate:
Idempotency-Keyplus a fingerprint of(filename, total_size, content_type)maps to oneupload_idfor a bounded window (documented, e.g., 24 hours); replaying the same initiate call returns the same session instead of creating a second one. - Chunk upload: idempotent by construction through
Content-Range. If a range that was already received arrives again with a matching checksum, the server responds200and does nothing further (safe retry after a lost response). If the same range arrives with a different checksum, that is a real conflict (409), not a retry, because the client is now sending different bytes for a range it already committed. - Finalize: idempotent on
upload_id; callingcompletetwice after success returns the same final file id both times rather than re-assembling or erroring, so a client that couldn't tell whether its firstcompletecall landed can safely call it again.
Handling intermittent mobile connectivity:
- Small chunk sizes (server hints a size, e.g., a few hundred kilobytes to a few megabytes) so a dropped connection loses at most one chunk's worth of progress, not the whole upload.
GET /uploads/{upload_id}is the resume contract: the client asks what ranges are already received and only re-sends what's missing, rather than restarting from byte zero.
Abandoned uploads and abuse:
expires_aton the session (returned at initiate time so the client can see its own deadline); a background sweeper deletes sessions and their stored chunks only afterexpires_athas passed, never touching a session with recentlast_activity_at.- Per-user quotas (maximum concurrent sessions, maximum total bytes in flight) enforced at initiate time, returning
429when exceeded, the same documented-quota pattern used for any other rate-limited endpoint. total_sizeis validated against the declared 200MB ceiling at initiate time, and each chunk's checksum is verified on arrival so a corrupted or tampered chunk is rejected before it is ever assembled into a final file.
Worked example
Initiate:
// POST /uploads Idempotency-Key: up-init-9f21
{ "filename": "trip-video.mp4", "total_size": 52428800, "content_type": "video/mp4" }
// 201 Created
{ "upload_id": "up_7c14", "expires_at": "2026-07-20T09:00:00Z", "chunk_size_hint": 262144 }
Upload the first chunk (256KB, matching the hinted chunk size, so the byte range is 0 through 262143 of the 52428800-byte total):
PATCH /uploads/up_7c14
Content-Range: bytes 0-262143/52428800
// 200 OK
{ "received_ranges": [[0, 262143]], "next_offset": 262144 }
Client drops off the network, reconnects later, and asks what it still needs:
GET /uploads/up_7c14
{ "upload_id": "up_7c14", "received_ranges": [[0, 262143]], "next_offset": 262144, "total_size": 52428800 }
It resumes exactly at byte 262144, no re-upload of the first chunk. Finalize once all ranges are in:
// POST /uploads/up_7c14/complete
{ "checksum_algorithm": "sha256", "checksum": "<client-computed full-file digest>" }
// 200 OK
{ "file_id": "file_31a0", "state": "completed" }
Trade-offs and pitfalls
- Server-proxied chunk uploads (as shown) are simpler to validate per-chunk but cost server bandwidth; direct-to-object-store multipart uploads (client uploads parts straight to storage using short-lived credentials) save that bandwidth but push part-tracking and checksum bookkeeping onto a more complex client-storage handshake. For 10k concurrent clients, the bandwidth savings usually win, at the cost of that added complexity.
- Chunk size is a real trade-off: smaller chunks tolerate flaky mobile networks better (less to re-send after a drop) but add per-chunk request overhead at scale; larger chunks are more efficient per byte but riskier on an unreliable connection.
- A pitfall specific to idempotency here: treating "same range, different checksum" as a silent overwrite instead of a
409hides a real client bug (a chunk that got corrupted or a retry that picked up stale data) and can quietly assemble a corrupted final file. - Garbage-collecting by
expires_atalone, without checkinglast_activity_at, risks deleting a session that is still actively (if slowly) being uploaded to from a poor connection right at the boundary of its TTL; the sweeper should treat both signals together, the same discipline as garbage-collecting any other long-lived job resource.
What is the difference between 'culture fit' and 'culture add', and which do you think better describes you as a candidate? Give one concrete example of a perspective, skill, or way of working you would bring to a team that is not already well represented there.
Sample Answer
Direct answer
Culture fit asks whether you already share a team's existing norms and behaviors; culture add asks what you would bring that the team does not already have. I would describe myself mostly as a culture add: I share the fundamentals a team needs to trust me (reliability, candor, respect for other people's time), but the useful thing I offer beyond that is a genuinely different working background rather than a mirror of the team that is already there.
Structured elaboration
- Define both terms precisely before answering for yourself. Culture fit is about alignment on shared behaviors and values: does this person operate the way we already operate. Culture add is about complementary difference: does this person's background, working style, or perspective fill a gap the team doesn't currently have.
- Explain why the distinction matters, not just define it. A team optimized purely for fit tends toward groupthink: everyone reasons the same way, so blind spots go unchallenged and the same kinds of mistakes recur. A team that only adds without any shared fit becomes uncoordinated: people can't predict each other's reasoning enough to move fast together. The healthy target is fit on a small number of load-bearing behaviors (honesty, follow-through, respect) plus deliberate add on everything else.
- Give a genuine, specific example of your own add, not a generic trait. Vague claims ("I bring diverse perspectives") are the single most common failure mode here; a strong answer names the concrete gap and the concrete evidence.
- Anticipate the natural follow-up: how do you know your difference is actually useful, versus just different for its own sake. The answer is to point at a specific decision, disagreement, or piece of feedback that changed because of the difference you brought, not just a credential or background fact.
Worked example
Suppose your last two teams were both product engineering teams building consumer-facing features, and the team you're interviewing for is mostly staffed by engineers with that same background. Your own prior role was on a data-platform team, closer to the systems that feed those consumer features than to the features themselves. A concrete add-story: in a past project, a product team wanted to ship a new recommendation feature quickly; because of your platform background, you asked a question the rest of the team hadn't raised (whether the upstream data pipeline's freshness guarantees actually matched what the feature's UI implied to users), which surfaced a real gap between a 24-hour batch refresh and a UI copy that said "updated just for you." The team fixed the copy and adjusted the refresh cadence before launch rather than after a user complaint. That is a genuine add: a different background produced a question the existing team composition was less likely to ask on its own, and it changed a real outcome.
Trade-offs & pitfalls
The common failure is answering only the definitional half (correctly explaining fit versus add) and then, when asked for a personal example, retreating to generic self-description ("I'm a good communicator", "I care about quality") that any candidate could say and that does not actually demonstrate difference. A second pitfall is overcorrecting into implying you don't fit at all; the strongest answers are explicit that you also share the small set of behaviors every functioning team needs, and that add is about everything on top of that baseline, not a replacement for it.
Before you commit to a technology you have not used, what do you actually do to find out whether it holds up? Take one check you would run and tell me how you would set it up, how long you would give it, and what result would settle the question.
Sample Answer
Direct answer
I pick one cheap, decisive check rather than trying to evaluate everything, I decide in advance exactly what result would change my mind in either direction, and I treat the answer as provisional until it survives a check against conditions close to my actual environment, not the vendor's easiest demo.
Structured elaboration
- Choose a check that's cheap and likely to be decisive, not exhaustive. Options I'd draw from: a load test against a traffic shape similar to what I'd actually see, a compatibility check against the messier parts of my real data, a "does the failure mode make sense" test where I deliberately break it and see what happens, or a rough cost-at-scale estimate. I pick whichever is most likely to actually kill the option if it's wrong, not whichever is easiest to run.
- Set it up against realistic conditions. As close to my real environment as is cheap to arrange, representative data volume and shape, realistic concurrency, rather than the vendor's polished happy-path demo.
- Time-box it with a fixed number of days. An evaluation with no deadline tends to drift on indefinitely, so I decide up front how long I'll give it.
- Pre-register the threshold before I see the result. I decide in advance what number or behavior counts as pass, fail, or genuinely needing more evidence. That's what makes an improvement believed rather than just accepted: I said in advance what would have counted as no improvement, so the result can actually surprise me.
- Keep the check away from anything that could hurt what's already running. It happens in an isolated environment that can't touch production, and if it passes, the first real use is staged behind a flag (a toggle that turns the new option on for only a slice of traffic, cheap to switch back off) or on a small, low-stakes slice, not a full rollout.
- Write the result down either way, pass or fail, so it isn't re-litigated from scratch the next time someone considers the same option.
Worked example
Say a team is considering a new caching layer that claims a large latency improvement over what they're currently running. The one check I'd pick is a load test against a replay of a real day's traffic, not a synthetic benchmark, because that's the check most likely to actually kill the claim if it doesn't hold up under real conditions rather than the vendor's ideal load pattern. I'd set a threshold before running it: the new layer needs to beat the current setup's latency at the ninety-fifth percentile, the level that reflects the slower end of typical requests rather than just the average, by a meaningful and pre-agreed margin under that same replayed traffic, or it's a no. I'd give it three days, run it in an isolated environment with no path to real traffic, and if it clears the threshold, roll it out first behind a flag on a small fraction of traffic with its own explicit monitoring before considering a wider switch. If it fails the threshold, I'd write that up too, so the option doesn't get re-proposed and re-tested from scratch in six months.
Trade-offs and pitfalls
The most common trap is trusting a benchmark the vendor ran under their own, more favorable conditions instead of your own. A close second is an open-ended "let's keep evaluating," where nobody ever set a threshold, so the check never actually resolves the decision either way. And testing directly against production instead of an isolated environment turns an evaluation into an incident risk, which defeats the purpose of a cheap, safe check in the first place.
In Kotlin, show how to start a second Activity with Intent extras and retrieve those extras safely in the receiving Activity. Include an example for a simple String extra and a Parcelable object. Demonstrate null-safety when reading extras and mention any preferred modern API for returning results to the caller.
Sample Answer
Brief approach
Show how to send extras (String + Parcelable) via Intent, read them in the receiving Activity in a null-safe way, and note the modern Activity Result API for returning results.
Sending Activity (MainActivity)
// create intent and start SecondActivity
val intent = Intent(this, SecondActivity::class.java).apply {
putExtra(SecondActivity.EXTRA_MESSAGE, "Hello from Main")
putExtra(SecondActivity.EXTRA_USER, User("alice", 30)) // User implements Parcelable
}
startActivity(intent)
// Preferred for returning results:
// val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> ... }
// launcher.launch(intent)
Receiving Activity (SecondActivity)
class SecondActivity : AppCompatActivity() {
companion object {
const val EXTRA_MESSAGE = "extra_message"
const val EXTRA_USER = "extra_user"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Null-safe retrieval
val message: String? = intent.getStringExtra(EXTRA_MESSAGE)
// Use safe-call or default:
val safeMessage = message ?: "Default message"
// Parcelable retrieval (null-safe). For API 33+, prefer passing Class<T> parameter.
val user: User? = intent.getParcelableExtra(EXTRA_USER)
// If you need non-null:
user?.let {
// use it
} ?: run {
// handle missing user
}
}
}
Parcelable example
@Parcelize
data class User(val name: String, val age: Int) : Parcelable
Null-safety & best practices
- Always treat extras as nullable and provide defaults or graceful handling.
- Use companion object constants for keys to avoid typos.
- For Android 13+ (API 33) prefer the overload:
intent.getParcelableExtra( key, User::class.java )
which returns a nullable User safely.
Returning results
- Prefer the Activity Result API (registerForActivityResult with ActivityResultContracts) over startActivityForResult for clearer lifecycle-safe code and typed contracts.
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