Meta Software Engineer (Mid-Level) Comprehensive Interview Preparation Guide
Meta's Software Engineer interview process for mid-level candidates consists of an initial recruiter screening, a technical phone screen focused on coding fundamentals, and a full onsite loop typically spanning 4-5 interviews across one or two days. The process evaluates technical depth, system design thinking, and cultural alignment with Meta's core values of moving fast and building long-term impact. Mid-level engineers are expected to demonstrate strong coding proficiency, foundational system design understanding, project ownership experience, and collaborative problem-solving abilities.
Interview Rounds
Recruiter Phone Screen
What to Expect
Your first interaction with Meta, typically lasting 20-30 minutes. The recruiter will have an informal conversation to assess your background, motivation for joining Meta, and cultural fit. This is not a technical screen but a fit assessment where the recruiter evaluates whether you understand Meta's mission and products, your career trajectory, and your genuine interest in the company. The recruiter will also ensure your experience level matches the role and discuss logistics for moving forward.
Tips & Advice
Research Meta thoroughly before this call—know their key products, recent initiatives, and business focus. Show genuine enthusiasm by mentioning specific Meta projects or innovations you admire. Bring enthusiasm and demonstrate that you've done your homework; even simple observations like following Meta's AI strategy or their work on metaverse technology help. Be concise and allow the recruiter to guide the conversation. Prepare 2-3 compelling stories about your background but keep them brief. Have thoughtful questions ready about the team and role.
Focus Topics
Career Goals and Growth Mindset
Articulate where you want to be in 5 years in terms of technical depth, leadership, impact, and skills you want to develop. Emphasize your drive to learn new technologies, take on challenging problems, and grow within the organization. Show how Meta's environment aligns with these goals.
Practice Interview
Study Questions
Problem-Solving and Impact Orientation
Share a brief story about a challenging problem you solved that had measurable impact. Focus on how you approached the problem, collaborated with others, and what you learned. Highlight the business or user impact of your solution.
Practice Interview
Study Questions
Motivation and Meta Alignment
Clearly explain why you want to join Meta specifically, not just any large tech company. Connect your career goals to Meta's mission of bringing people closer together, their technology stack, or specific products. Show understanding of Meta's scale, impact, and business challenges.
Practice Interview
Study Questions
Professional Background and Journey
Articulate your career progression, highlighting relevant projects, technologies used, and measurable outcomes. For mid-level candidates, emphasize projects you've owned independently and how you grew from early to mid-career stages. Include specific examples of technical growth, scope increase, and increased responsibility.
Practice Interview
Study Questions
Understanding Meta's Products and Mission
Demonstrate familiarity with Meta's core products (Facebook, Instagram, WhatsApp, Threads), emerging initiatives (AI/Llama, metaverse, AR/VR), and business model. Show that you've used their products and understand Meta's role in the tech industry. Reference recent innovations or strategic decisions that resonate with you.
Practice Interview
Study Questions
Technical Phone Screen
What to Expect
A 45-minute technical coding assessment conducted by a Meta engineer via phone/video. You'll typically receive one or two coding problems of medium difficulty (similar to LeetCode medium). The interviewer will assess your ability to understand the problem, communicate your approach, write clean code, handle edge cases, and explain your complexity analysis. This round tests fundamental coding skills, data structures knowledge, and problem-solving methodology. Communication is as important as correctness—explain your thinking process clearly before and while coding.
Tips & Advice
Before writing any code, restate the problem to ensure understanding and discuss your approach with the interviewer. Start with a brute force solution if needed, then optimize. Always explain time and space complexity for both approaches. Write clean, readable code with proper variable names. Test your code with provided examples and consider edge cases. Communicate constantly—explain what you're doing and why. Practice solving problems in exactly 45-minute windows using LeetCode. For Python specifically, focus on writing concise and efficient code while clearly explaining your reasoning. If you get stuck, think out loud and ask clarifying questions rather than sitting in silence.
Focus Topics
Python-Specific Optimization
For Python interviews, know built-in libraries and functions (collections, itertools, etc.). Write Pythonic code using list comprehensions, generator expressions, and idiomatic Python. Understand Python's data structures (list, dict, set, tuple) and their performance characteristics. Write concise code without sacrificing readability.
Practice Interview
Study Questions
Code Quality and Edge Case Handling
Write clean code with meaningful variable names, proper indentation, and logical flow. Handle edge cases: empty inputs, single elements, negative numbers, duplicates, null values, boundary conditions. Think through and test edge cases before submitting code.
Practice Interview
Study Questions
Communication and Explanation
Verbally explain your thought process throughout the interview. Discuss your approach before coding. Narrate what you're doing while coding. Explain your complexity analysis clearly. Ask for feedback and adjust if needed. Use precise technical language while avoiding unnecessary jargon.
Practice Interview
Study Questions
Data Structures Fundamentals
Master core data structures including arrays, strings, linked lists, stacks, queues, hash tables, trees (binary, BST), and graphs. Understand insertion, deletion, search operations and their time complexities. Know when to use each data structure and why. For mid-level, be comfortable implementing these from scratch when needed.
Practice Interview
Study Questions
Problem-Solving Methodology
Develop a structured approach: understand the problem, ask clarifying questions, outline approach, code incrementally, test. Practice identifying problem patterns (two pointers, sliding window, depth-first search, breadth-first search, dynamic programming, greedy, etc.). Learn when to apply each pattern.
Practice Interview
Study Questions
Algorithm Complexity Analysis
Deeply understand Big O notation and complexity analysis. Be able to calculate time and space complexity for any solution you write. Know the complexity of common algorithms (sorting: O(n log n), searching: O(log n) vs O(n), etc.). Understand trade-offs between time and space complexity.
Practice Interview
Study Questions
Onsite Technical Interview 1 - Coding
What to Expect
The first of typically two technical interviews during your full day onsite at Meta. This 45-60 minute interview follows the same format as the phone screen but expects slightly harder medium-level problems that may involve multiple steps or subtle edge cases. You'll solve a problem from scratch on a shared document or whiteboard. The interviewer assesses your coding ability, problem-solving approach, complexity analysis, and communication. There may also be follow-up questions about how to extend your solution or handle different scenarios.
Tips & Advice
Approach this like the phone screen but with elevated expectations. The problem may require multiple data structures or algorithms combined. Take time to understand the full problem before jumping to code—ask clarifying questions about constraints, input sizes, and expected output format. Start with a clear approach, code methodically, test thoroughly. After solving the base problem, expect variations like 'How would you handle concurrent access?' or 'What if the input was sorted?' Be ready to optimize further. Time management is crucial—ensure you have time to test your code. Stay calm under pressure and maintain clear communication throughout.
Focus Topics
Testing and Validation
Before submitting your solution, test it with provided examples and edge cases you identify. Walk through your logic with test cases to catch bugs. Explain what you're testing and why. Be willing to fix bugs found during testing.
Practice Interview
Study Questions
Handling Ambiguity and Problem Variation
Expect interviewers to ask follow-up questions that change the problem slightly or add constraints. Respond by understanding the change, adjusting your approach, and communicating how your solution handles it. Be flexible and show ability to adapt your thinking.
Practice Interview
Study Questions
Advanced Data Structures
Be comfortable with trees (binary search trees, balanced trees, tries), graphs (directed/undirected, weighted/unweighted), heaps, and advanced hash table usage. Know algorithms for these structures: traversals (DFS, BFS), shortest path, cycle detection, topological sort.
Practice Interview
Study Questions
Multi-Step Problem Solving
Handle problems that require combining multiple algorithms or data structures. Example: use a graph algorithm on top of a hash table to solve a problem. Decompose complex problems into manageable steps and solve each systematically. Verify each step works before proceeding.
Practice Interview
Study Questions
Optimization and Trade-offs
After solving a problem, identify optimization opportunities. Discuss trade-offs: time vs space, readability vs performance, simplicity vs efficiency. Practice moving from O(n²) to O(n), or finding the optimal space-time complexity. Understand when further optimization isn't worth the complexity.
Practice Interview
Study Questions
Onsite Technical Interview 2 - Coding
What to Expect
The second technical interview during your onsite day, following the same 45-60 minute format as Interview 1. This interview assesses consistency of your coding ability and whether you remain sharp after multiple interviews. You may face a different problem type or difficulty level. Some candidates report this round being slightly easier to allow recovery if the first round was challenging, though this isn't guaranteed. The interviewer is looking for the same signals: problem-solving, clear communication, code quality, complexity analysis, and handling edge cases.
Tips & Advice
Treat this interview fresh—don't carry over frustration from the first interview. Use the same structured approach: understand problem, communicate approach, code carefully, test thoroughly. You may be more tired by this point, so manage your energy: take deep breaths, move around if possible, drink water. If this problem feels easier than the first, great—execute it cleanly and efficiently. If it's harder, apply problem-solving fundamentals and think out loud. Interviewers evaluate consistency, so maintain the same quality of communication and code. Remember you've already passed one interview; use that confidence.
Focus Topics
Debugging and Error Recovery
During coding, if you notice a bug, calmly trace through your code to identify the issue. Fix bugs by modifying your solution logically rather than rewriting. Explain your debugging process to the interviewer. If completely stuck, ask for hints rather than spending excessive time in silence.
Practice Interview
Study Questions
Consistency Across Multiple Rounds
Maintain consistent performance, communication quality, and code cleanliness across both technical interviews. If the first was strong, replicate that quality. If the first was weak, learn from it and improve in the second.
Practice Interview
Study Questions
String and Array Manipulation
Practice problems involving string transformations, pattern matching, array reorganization, and substring operations. Understand string vs array performance differences. Know library functions but also be able to implement core operations manually.
Practice Interview
Study Questions
Recursion and Backtracking
Understand how to structure recursive solutions with proper base cases and recursive cases. Practice problems involving combinations, permutations, and subset generation. Understand call stacks and recursion depth. Know when recursion is appropriate vs iterative solutions.
Practice Interview
Study Questions
LeetCode Medium Problem Patterns
Master common problem patterns: two pointers, sliding window, fast/slow pointers, depth-first search, breadth-first search, binary search, dynamic programming, greedy algorithms, backtracking. For each pattern, practice 3-5 problems until you can recognize and solve them quickly.
Practice Interview
Study Questions
Onsite System Design Interview
What to Expect
A 45-60 minute interview assessing your ability to design scalable systems. You'll be given a vague problem like 'Design Instagram photo storage' or 'Design a URL shortening service,' and expected to ask clarifying questions, outline high-level architecture, discuss trade-offs, and dive into specific components as needed. This round evaluates your understanding of distributed systems concepts, scalability considerations, database design, caching, and API design. For mid-level engineers, the focus is on fundamental system design thinking and ability to communicate your approach, not necessarily perfect architectural decisions.
Tips & Advice
Start by asking clarifying questions about scale (users, QPS, storage), requirements (functional and non-functional), and constraints. Don't jump into drawing immediately—discuss your approach first. Use diagrams and clearly labeled components. Focus on explaining your reasoning and trade-offs: why this database over that one, why caching here, etc. Be comfortable saying 'I don't know' and discussing options when uncertain. Discuss scalability bottlenecks and how you'd address them. For mid-level, the interviewer expects sound reasoning but not perfect architectural choices. Walk through how your system handles growth and load. Avoid overcomplicating—start simple and add complexity where needed.
Focus Topics
Communication and Collaboration
Verbally walk through your design as you build it. Explain component choices. Draw clear diagrams with labeled components and communication flows. Invite interviewer feedback and adjust your design if they point out issues. Discuss your thinking, not just presenting a finished design.
Practice Interview
Study Questions
Distributed Systems Fundamentals
Understand basic distributed systems concepts: load balancing, replication, fault tolerance, consistency models (strong, eventual, causal). Know challenges like network partitions, data duplication, and how systems handle failures.
Practice Interview
Study Questions
Caching Strategies
Understand caching layers (in-memory caches like Redis/Memcached), cache invalidation strategies (TTL, LRU, write-through, write-behind), and when caching is beneficial. Know cache hit rate and its impact on system performance.
Practice Interview
Study Questions
Trade-off Analysis
For architectural decisions, explicitly discuss trade-offs: consistency vs availability, latency vs throughput, simplicity vs efficiency, cost vs performance. Explain why you chose one approach over another given specific requirements.
Practice Interview
Study Questions
Scalability and Capacity Planning
Estimate system requirements: how many users, queries per second (QPS), storage needed. Calculate bandwidth requirements. Understand how these numbers drive architectural decisions. Know rule-of-thumb estimations (e.g., 1 million users might mean 10-100 QPS). For mid-level, be able to do back-of-the-envelope calculations and justify architectural choices based on scale.
Practice Interview
Study Questions
Database Selection and Design
Understand SQL vs NoSQL trade-offs and when to use each. Know properties of different databases: relational databases (ACID), NoSQL databases (eventual consistency, partition tolerance). Practice database schema design for given problems. Understand sharding strategies for horizontal scaling.
Practice Interview
Study Questions
Onsite Behavioral and Hiring Manager Interview
What to Expect
A 45-60 minute interview combining behavioral assessment with a hiring manager conversation. The interviewer (typically your future manager or a senior engineer) explores your past experiences, how you handled challenges, collaboration style, and alignment with Meta's core values. This round evaluates your cultural fit, growth mindset, ability to work cross-functionally, and whether you'd be good to work with. Meta emphasizes their values: Move Fast, Focus on Long-Term Impact, and Build Awesome Things. You'll discuss specific projects, how you handled failures, and your leadership/mentorship approach. This is also your opportunity to assess whether Meta is right for you.
Tips & Advice
Prepare 4-6 detailed stories about your professional experiences using the STARR framework: Situation, Task, Action, Result, Reflection. Each story should illustrate different aspects: handling ambiguity, collaboration, learning from failure, achieving impact, mentoring, or overcoming challenges. Relate your stories back to Meta's core values where possible. Be authentic—interviewers can tell when you're being dishonest. Show genuine interest in the team and role. Ask thoughtful questions about how the team operates, their technical challenges, and culture. For mid-level, emphasize project ownership, collaboration across teams, and growing into leadership. Listen carefully to questions and answer directly. If asked 'Tell me about yourself,' focus on professional journey, key achievements, and why you're excited about Meta. Show growth mindset—discuss what you learned from failures, not just successes.
Focus Topics
Handling Ambiguity and Problem-Solving
Describe situations with unclear requirements, competing priorities, or ambiguous technical challenges. Discuss how you clarified requirements, made decisions with incomplete information, or pivoted your approach. Show comfort with ambiguity and systematic problem-solving.
Practice Interview
Study Questions
Learning from Failure and Adaptability
Share stories about significant failures or setbacks: a project that failed, a technical decision that didn't work out, or a situation where your approach was wrong. Focus on what you learned, how you adapted, and how you applied lessons. Show resilience and growth mindset.
Practice Interview
Study Questions
Mentorship and Development of Others
As a mid-level engineer, you're expected to mentor junior engineers or contribute to team growth. Share stories about helping junior colleagues, code reviews that provided learning, or knowledge sharing. Discuss how you approach mentorship and what you've learned from helping others grow.
Practice Interview
Study Questions
Meta's Core Values Integration
Understand Meta's core values: Move Fast (moving quickly without being reckless), Focus on Long-Term Impact (thinking beyond immediate solutions), and Build Awesome Things (creating quality products with craftsmanship). In behavioral stories, weave these values naturally. Demonstrate how your past work exemplifies one or more of these values. Don't just name-drop values; show them through your actions and decisions in stories.
Practice Interview
Study Questions
Project Ownership and Impact
Share stories where you owned projects end-to-end from conception to deployment. Describe the problem, your technical solution, how you drove it to completion, and measurable impact (user adoption, performance improvement, reduced costs). Emphasize taking ownership even when spanning multiple teams. Use metrics to quantify impact.
Practice Interview
Study Questions
Cross-Functional Collaboration
Provide examples of working effectively with product managers, designers, other engineers, or operations teams. Discuss how you communicated technical concepts to non-technical stakeholders, navigated disagreements, and reached consensus. Show you can collaborate across boundaries.
Practice Interview
Study Questions
Frequently Asked Software Engineer Interview Questions
Provide two analogies you could use to explain the CAP theorem to a product manager who is not a software engineer. For each analogy, say which part of CAP it captures well and where it breaks down.
Sample Answer
Direct answer
CAP theorem (Consistency, Availability, Partition tolerance) says that when a distributed system's network partitions, some nodes cannot talk to others, you must choose between staying available (keep answering requests) or staying consistent (guarantee every reader sees the latest write); you cannot fully guarantee both during that partition. For a product manager, the useful frame is not the three-letter acronym, it is the trade-off it forces: during a network problem, do we serve possibly-stale data, or do we go silent until we're sure the data is correct? Two analogies below make that concrete, plus where each one starts to mislead.
How to build and stress-test an analogy like this
- Start from something the audience already manages themselves so the coordination problem is intuitive without teaching new vocabulary.
- Map only the DECISION the concept forces (here, what happens when parts can't talk), not every mechanism. If you find yourself trying to represent quorum writes or version numbers in the analogy, you've picked the wrong analogy or gone too deep.
- Stress-test it before using it: ask yourself what a sharp follow-up question would reveal is wrong with it. If it has no honest breaking point, you haven't tested it hard enough, you've only used it once.
- Name the breaking point out loud, before they find it. That's the senior move: it turns a limitation into evidence you understand the real system, instead of a gotcha that undermines the analogy later.
- The same loop, familiar system, one decision, explicit breaking point, works for any concept in this family: explaining algorithmic complexity (Big-O) to a PM, a model's bias/variance trade-off to a stakeholder, why a prediction leans on certain inputs (SHAP values), or why Raft consensus needs a leader election before it can make progress.
Worked example
Analogy 1: bank branches during a network outage. A bank has several branches connected by a private network. A customer withdraws money at Branch A. If the network to Branch B is up, Branch B's ledger updates immediately, every branch shows the correct new balance (Consistency). If a cable gets cut between the branches (a partition), Branch B has two choices: let customers keep withdrawing using its last-known balance (Availability, but the balance might be wrong), or refuse withdrawals until the network is fixed and balances can be confirmed (Consistency, but Branch B is unavailable). What it captures well: the forced, binary choice under a partition, and that it's a business decision, not a bug to fix. Where it breaks: real banks resolve most of this with human reconciliation and legal recourse, an incorrect balance gets corrected by staff, with clear liability rules. Distributed databases usually make this choice automatically, in milliseconds, with no human in the loop, so the "someone will sort it out later" comfort the analogy implies isn't actually available.
Analogy 2: two people, one shared paper shopping list, two different stores. You and a partner keep a shared shopping list at home but each take a photo before heading to a different grocery store. While your phones have signal, any item one of you crosses off can be relayed to the other, so the list stays in sync (Consistency). If both phones lose signal at once (a partition), you each keep shopping off your own photo, you stay productive (Availability), but you risk both buying milk, or neither of you buying it, because neither photo reflects the other's crossed-off items. What it captures well: a partition doesn't stop work, it stops coordination, and the resulting inconsistency is a direct, visible consequence of choosing to stay available. Where it breaks: reconciling two shopping lists is cheap and forgiving, worst case you return the extra milk. Reconciling two halves of a financial ledger or an inventory count is not cheap or forgiving in the same way, so the analogy understates how expensive real clean-up can be.
Trade-offs and pitfalls
Don't let either analogy imply CAP is a permanent, top-level architecture choice; it applies at the moment of a partition, and most systems are both consistent and available the rest of the time. That's the single most common misunderstanding a PM walks away with if you aren't explicit about it. Also resist collapsing CAP into "consistency vs speed," that conflates it with the separate latency/consistency trade-offs many systems make even without a partition. And don't use the analogy to make the decision for the PM, the job here is to make the trade-off legible so they can weigh it against the product's actual tolerance for stale data.
You're weighing two possible investments: an algorithmic change that would cut CPU time by 30%, or a systems fix that would cut network serialization time by 60% but is riskier to deploy. How would you decide which one is actually worth doing first?
Sample Answer
Approach
Weigh each option by how much it actually contributes to what matters end-to-end, not by its own narrow percentage, then treat risk and reversibility as the tiebreaker when the raw numbers are close.
Worked example (illustrative numbers)
Suppose CPU time is 50% of total request time and network serialization (the work of converting data to and from a format for sending over the network) is 25%, with the rest being I/O wait or other work. The algorithmic change, a 30% cut on the 50% component, yields roughly 0.5 times 30%, about 15% overall improvement. The systems fix, a 60% cut on the 25% component, yields roughly 0.25 times 60%, also about 15% overall. On paper, they are a wash, which is exactly the situation where risk and reversibility should decide it, not the headline percentages.
Making the risk concrete
Rather than treat "riskier to deploy" as a vague minus, I would ask specifically why: does it touch shared serialization logic other services depend on, is it hard to test at production scale beforehand, or is rollback difficult if something goes wrong. If the algorithmic change is comparably impactful and lower risk, contained to one component, testable in isolation, easy to roll back, I would do that first, bank the equivalent 15% win at lower risk, and either postpone the systems fix or de-risk it, for example shipping it behind a flag to a small percentage of traffic with a fast rollback path, before committing to it fully.
When I'd flip the order
If the systems fix's real-world upside turned out to be meaningfully larger than the algorithmic change once actually measured, not just estimated, that would justify taking it first despite the risk. But I would not choose the higher-risk option on paper numbers alone when a comparable, safer win is available right now.
Explain the differences between Least Recently Used (LRU) and Least Frequently Used (LFU) eviction policies. For an in-memory cache storing user session objects that are frequently accessed soon after login then rarely, which policy is more appropriate and why?
Sample Answer
Direct answer
Least Recently Used (LRU) evicts based on recency of access and fits workloads where "recently touched" predicts "will be touched again soon"; Least Frequently Used (LFU) evicts based on access count and fits workloads where a stable set of items is popular over a long time, even if not accessed in the last few seconds.
Structured elaboration
- LRU mechanics and fit: LRU tracks the order items were last accessed and evicts the item that has gone the longest without a touch. It fits access patterns with strong temporal locality: a session object, for example, is heavily accessed right after login and then goes cold; LRU naturally keeps the currently-active sessions warm and evicts the ones nobody has touched recently.
- LFU mechanics and fit: LFU tracks how many times each item has been accessed (often with some decay over time to avoid permanently favoring old-but-once-popular items) and evicts the least-accessed item. It fits patterns with a stable long-tail of popular items that get accessed periodically but not necessarily continuously, such as a small set of perennially popular catalog items that get read constantly, interspersed with occasional cold reads of rare items; a pure LRU cache would wrongly evict a genuinely popular item just because it happened not to be touched in the last few seconds.
- LRU's weak spot: a single burst of one-time scans (e.g., a batch job reading every record once) can flush an LRU cache of genuinely hot items, because the scan makes every item "recently used" once, pushing out items that are accessed far more often over time.
- LFU's weak spot: new items start at zero frequency and can be evicted immediately even if they would become popular, because they have not yet accumulated enough hits to compete with established items (this is sometimes mitigated with a decay factor or a frequency-boosted admission window).
Worked example
For user session objects that are heavily read right after login and then rarely touched again: LRU is the right default, because the access pattern IS recency-driven, and LFU would keep old, no-longer-relevant sessions around simply because they accumulated a lot of hits while they were active. For a product catalog with seasonal hot items plus a long tail of rarely-viewed products: LFU (or a hybrid) does better, because a genuinely popular item accessed every few minutes should survive occasional bursts of one-time-scan traffic on unrelated items that would otherwise flush it from an LRU cache.
Trade-offs and pitfalls
LFU requires more bookkeeping (a counter per item, often with decay logic) than LRU's simple access-order list, so it costs more memory and CPU per operation; do not reach for LFU unless the access pattern genuinely benefits from it. Neither pure policy handles every real workload well, which is why production caches (Redis's allkeys-lfu, or an adaptive scheme like W-TinyLFU) increasingly blend recency and frequency signals rather than using either in isolation.
Implement a recursive function that returns all permutations of a list of distinct integers without using library permutation helpers. Show the output for [1, 2, 3], explain how you avoid shared-state bugs, and state the time and space complexity.
Sample Answer
The idea in plain words
A permutation is an ordering of all the items. For n distinct items there are n! of them (n choices for the first slot, n-1 for the next, and so on). The backtracking idea: fill the slots from left to right; at each slot, try every item that has not been used yet, recurse to fill the remaining slots, then take the item back so the next candidate can try the same slot.
The brute-force alternative, generating all n^n sequences and discarding the ones with repeats, wastes almost all of its work. Backtracking never builds a repeat in the first place.
Approach 1: a used array
Keep path (the arrangement so far) and used[i] (whether nums[i] is already in path). A full-length path is one permutation.
Approach 2: swap in place and swap back
Treat the array itself as the state. Slots before index first are fixed. To fill slot first, swap each candidate arr[i] (for i >= first) into that slot, recurse on first + 1, then swap back. This needs no used array and no separate path list.
from itertools import permutations
from typing import List
def permutations_used(nums: List[int]) -> List[List[int]]:
result: List[List[int]] = []
path: List[int] = []
used = [False] * len(nums)
def backtrack() -> None:
if len(path) == len(nums):
result.append(path[:]) # copy: path keeps changing
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True # choose
path.append(nums[i])
backtrack() # explore
path.pop() # undo
used[i] = False
backtrack()
return result
def permutations_swap(nums: List[int]) -> List[List[int]]:
arr = nums[:] # work on a copy of the input
result: List[List[int]] = []
def backtrack(first: int) -> None:
if first == len(arr):
result.append(arr[:])
return
for i in range(first, len(arr)):
arr[first], arr[i] = arr[i], arr[first] # choose: put arr[i] at slot first
backtrack(first + 1) # explore
arr[first], arr[i] = arr[i], arr[first] # undo: swap back
backtrack(0)
return result
print(permutations_used([1, 2, 3]))
print(permutations_swap([1, 2, 3]))
for n in range(0, 7):
items = list(range(n))
expected = [list(p) for p in permutations(items)]
assert permutations_used(items) == expected
assert sorted(permutations_swap(items)) == sorted(expected)
print("checks passed")
print("empty list:", permutations_used([]), permutations_swap([]))
def leaky(nums):
result, path, used = [], [], [False] * len(nums)
def backtrack():
if len(path) == len(nums):
result.append(path) # BUG: no copy
return
for i in range(len(nums)):
if not used[i]:
used[i] = True; path.append(nums[i])
backtrack()
path.pop(); used[i] = False
backtrack()
return result
print("without copying:", leaky([1, 2, 3]))
def swap_no_undo(nums):
arr, result = nums[:], []
def backtrack(first):
if first == len(arr):
result.append(arr[:])
return
for i in range(first, len(arr)):
arr[first], arr[i] = arr[i], arr[first]
backtrack(first + 1)
# BUG: no swap back here
backtrack(0)
return result
print("swap without swapping back:", swap_no_undo([1, 2, 3]))
Output (the itertools import is only used by the check against the standard library; the solutions do not call it):
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]
checks passed
empty list: [[]] [[]]
without copying: [[], [], [], [], [], []]
swap without swapping back: [[1, 2, 3], [1, 3, 2], [3, 1, 2], [3, 2, 1], [1, 2, 3], [1, 3, 2]]
Both methods find the same six permutations. For a sorted input the used version yields them in lexicographic order, meaning dictionary order: compare first items, and break ties with the second item, then the third (the check above compares it, in order, with itertools.permutations for range(n) inputs). The swap version produces [3, 2, 1] before [3, 1, 2] because swapping reorders the tail (the swap trace below shows exactly where), so compare results as sets or sorted lists, not by position.
Trace of the used version on [1, 2, 3]
This instrumented version of the used recursion prints each choose and undo, indented by depth (depth only controls the indentation); the swap version's instrumented copy follows it in the same block:
from typing import List
def permutations_used_traced(nums: List[int]) -> None:
path: List[int] = []
used = [False] * len(nums)
def backtrack(depth: int) -> None:
pad = " " * depth
if len(path) == len(nums):
print(f"{pad}full {path} -> record")
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
path.append(nums[i])
print(f"{pad}choose {nums[i]} path={path}")
backtrack(depth + 1)
path.pop()
used[i] = False
print(f"{pad}undo {nums[i]} path={path}")
backtrack(0)
def permutations_swap_traced(nums: List[int]) -> None:
arr = nums[:]
def backtrack(first: int) -> None:
pad = " " * first
if first == len(arr):
print(f"{pad}full {arr} -> record")
return
for i in range(first, len(arr)):
arr[first], arr[i] = arr[i], arr[first]
print(f"{pad}swap slots {first},{i} arr={arr}")
backtrack(first + 1)
arr[first], arr[i] = arr[i], arr[first]
print(f"{pad}swap back {first},{i} arr={arr}")
backtrack(0)
permutations_used_traced([1, 2, 3])
print("--- swap version ---")
permutations_swap_traced([1, 2, 3])
The first half of its output is the used trace:
choose 1 path=[1]
choose 2 path=[1, 2]
choose 3 path=[1, 2, 3]
full [1, 2, 3] -> record
undo 3 path=[1, 2]
undo 2 path=[1]
choose 3 path=[1, 3]
choose 2 path=[1, 3, 2]
full [1, 3, 2] -> record
undo 2 path=[1, 3]
undo 3 path=[1]
undo 1 path=[]
choose 2 path=[2]
choose 1 path=[2, 1]
choose 3 path=[2, 1, 3]
full [2, 1, 3] -> record
undo 3 path=[2, 1]
undo 1 path=[2]
choose 3 path=[2, 3]
choose 1 path=[2, 3, 1]
full [2, 3, 1] -> record
undo 1 path=[2, 3]
undo 3 path=[2]
undo 2 path=[]
choose 3 path=[3]
choose 1 path=[3, 1]
choose 2 path=[3, 1, 2]
full [3, 1, 2] -> record
undo 2 path=[3, 1]
undo 1 path=[3]
choose 2 path=[3, 2]
choose 1 path=[3, 2, 1]
full [3, 2, 1] -> record
undo 1 path=[3, 2]
undo 2 path=[3]
undo 3 path=[]
Read it as: after choose 1, the slots 2 and 3 are filled by every arrangement of {2, 3}; undo 1 returns path to empty so the first slot can try 2.
Trace of the swap version on [1, 2, 3]
The second half of the same program's output, after the --- swap version --- line. Each line shows the whole array arr; indentation equals first, the number of slots already fixed:
swap slots 0,0 arr=[1, 2, 3]
swap slots 1,1 arr=[1, 2, 3]
swap slots 2,2 arr=[1, 2, 3]
full [1, 2, 3] -> record
swap back 2,2 arr=[1, 2, 3]
swap back 1,1 arr=[1, 2, 3]
swap slots 1,2 arr=[1, 3, 2]
swap slots 2,2 arr=[1, 3, 2]
full [1, 3, 2] -> record
swap back 2,2 arr=[1, 3, 2]
swap back 1,2 arr=[1, 2, 3]
swap back 0,0 arr=[1, 2, 3]
swap slots 0,1 arr=[2, 1, 3]
swap slots 1,1 arr=[2, 1, 3]
swap slots 2,2 arr=[2, 1, 3]
full [2, 1, 3] -> record
swap back 2,2 arr=[2, 1, 3]
swap back 1,1 arr=[2, 1, 3]
swap slots 1,2 arr=[2, 3, 1]
swap slots 2,2 arr=[2, 3, 1]
full [2, 3, 1] -> record
swap back 2,2 arr=[2, 3, 1]
swap back 1,2 arr=[2, 1, 3]
swap back 0,1 arr=[1, 2, 3]
swap slots 0,2 arr=[3, 2, 1]
swap slots 1,1 arr=[3, 2, 1]
swap slots 2,2 arr=[3, 2, 1]
full [3, 2, 1] -> record
swap back 2,2 arr=[3, 2, 1]
swap back 1,1 arr=[3, 2, 1]
swap slots 1,2 arr=[3, 1, 2]
swap slots 2,2 arr=[3, 1, 2]
full [3, 1, 2] -> record
swap back 2,2 arr=[3, 1, 2]
swap back 1,2 arr=[3, 2, 1]
swap back 0,2 arr=[1, 2, 3]
Read it as: at first = 0, swapping slot 0 with slot 2 turns [1, 2, 3] into [3, 2, 1], so the free tail is now [2, 1] and its first arrangement [3, 2, 1] is recorded before the swap at slots 1,2 produces [3, 1, 2]. That is why [3, 2, 1] precedes [3, 1, 2]. Each swap back line restores the array to what it was before the matching swap, so the next candidate starts from a clean state.
How shared-state bugs are avoided
- Copy on record:
path[:]orarr[:]stores a snapshot. Thewithout copyingline of the output shows what happens without it: six entries that are all the same (now empty) list. - Symmetric undo: every
choosehas an exact reverse right after the recursive call (path.pop()plusused[i] = False; the second swap). If one half is missing, later branches see leftovers. - The input is not mutated: the swap version copies
numsfirst, so the caller's list is unchanged.
Complexity
- Time: there are n! permutations, each copied in O(n) when recorded, so O(n * n!) just for the output. The search tree (every call, partial and full) has about e * n! nodes in total, where e is the constant 2.718... . At depth j (j slots filled) there are n!/(n-j)! nodes: choices for the first slot times choices for the second, and so on. For n = 3 the depths 0, 1, 2, 3 have 1, 3, 6, 6 nodes, which sum to 16, and 16 / 3! = 2.67. That ratio is already near e and climbs toward it: adding n!/(n-j)! for j = 0 to 7 gives 13700 nodes for n = 7, and 13700 / 7! = 2.7183. So the whole tree is only about 2.7 times the number of leaves. The
usedversion scans n slots at each node, so it is also O(n * n!) overall. The swap version does O(1) work per child (no scan), also O(n * n!) because of the copies. - Space: O(n) auxiliary (recursion depth n, plus
pathandused), not counting the output, which is n * n! numbers. If the caller does not need every permutation at once, a generator that yields each one keeps memory at O(n).
Edge cases and common mistakes
- Empty list returns
[[]](one permutation, the empty one); a single item returns[[x]]. - With duplicate values both routines emit repeated permutations. For the
usedversion, sort the input and skipnums[i]when it equalsnums[i - 1]andused[i - 1]is false. That sorted-neighbour rule does not carry over to the swap version, because swaps move values around and equal values stop being neighbours; there, keep a set of the values already tried in the current slot and skip any repeat. - Using
used = []as a list of values and testingx in usedmakes each check O(n); a boolean array is O(1). - Forgetting to copy (the
without copyingline) or forgetting to swap back (theswap without swapping backline, which lists[1, 2, 3]and[1, 3, 2]twice and never produces[2, 1, 3]or[2, 3, 1]).
Running the code
python3.12 permutations_backtracking.py
Save the program above as permutations_backtracking.py. No packages are needed (standard library only).
Design a load-testing plan to validate increasing in-replica concurrency for a model server (from 2 to 8 concurrent requests per process). Describe test scenarios, the observability signals you would capture, chaos testing elements, and clear acceptance criteria for the change.
Sample Answer
Test scenarios
Establish a baseline at today's concurrency (2 concurrent requests per process) to get today's real latency, throughput, and error numbers, then step through intermediate levels (2, then 4, then 6, then 8) instead of jumping straight to 8. Per-process resource contention, GPU memory pressure, thread contention, internal request batching, often degrades non-linearly, and stepping through finds the actual knee in the curve rather than just confirming the endpoint works or doesn't. Add a sustained soak run AT the target concurrency (8) over a longer window, since some failure modes, GPU memory fragmentation from many small concurrent allocations, gradual memory growth in a long-lived worker process, only surface after sustained concurrent use, not a short burst.
Observability signals to capture
- Latency percentiles (p50/p95/p99, the response times 50%, 95%, and 99% of requests finish faster than respectively; not just an average) at each concurrency step, tail latency is what users actually notice and averages hide it.
- GPU/CPU utilization and memory per process, to see whether higher concurrency is genuinely using more of the accelerator or just queuing behind contention.
- Request queue depth and time-in-queue, if the server batches requests internally.
- Throughput (successful inferences per second) at each step.
- Error and timeout rate at each step, since a server can appear to "handle" higher concurrency by silently dropping or timing out requests rather than genuinely serving them faster.
Chaos testing elements
Kill and restart the model server process mid-load-test to confirm it recovers cleanly at the new concurrency setting without leaking GPU memory from the killed process's in-flight requests. If the model calls out to a downstream dependency (a feature store, another service), briefly saturate that dependency to see whether higher in-process concurrency amplifies a downstream failure into a bigger blast radius than it would at concurrency 2.
Acceptance criteria
- p99 latency at concurrency 8 stays within an agreed bound of the concurrency-2 baseline (tune the exact percentage to the actual SLO, for example no more than 25% higher).
- Throughput per process genuinely increases with concurrency; if going from 2 to 8 doesn't meaningfully raise inferences per second, the process is I/O- or lock-bound and raising concurrency is just adding queuing delay without real gain, that's a finding worth reporting even if it means NOT making the change.
- Error rate stays flat across all steps.
- The process survives the soak run without unbounded memory growth or a crash.
Explain what a CRDT (Conflict-free Replicated Data Type) is and why state-based and operation-based CRDTs let replicas converge to the same value without any coordination between them. Walk through two concrete examples: a grow-only counter (G-Counter) and an observed-remove set, and describe what property of the underlying merge operation makes convergence guaranteed.
Sample Answer
A CRDT (Conflict-free Replicated Data Type) is a data structure whose update and merge operations are mathematically guaranteed to make every replica converge to the same value, with no locking, coordination, or central authority, as long as every update eventually reaches every replica. State-based CRDTs ship the whole replica state and merge it with a commutative, associative, idempotent join function; operation-based CRDTs ship individual operations that must themselves be commutative and be delivered with causal ordering. A grow-only counter (G-Counter) and an observed-remove set (OR-Set) are the two simplest concrete examples of this guarantee in action.
Why convergence is guaranteed
Convergence works because the merge operation is commutative (order doesn't matter), associative (grouping doesn't matter), and idempotent (merging a state with itself changes nothing), so applying merges in any order, any number of times, produces the same final state. Formally, this kind of merge is called a join, and a replica's state is modeled as an element of a join-semilattice: a partially ordered set where the join always computes the least upper bound of two states. That is the actual property behind convergence without coordination: it is not that conflicts never happen, it is that the merge function is defined so a conflict has exactly one well-defined resolution no matter how or when it gets computed.
G-Counter (grow-only counter)
- State: a vector with one non-negative integer slot per replica, c[i].
- Local update: a replica only ever increments its own slot.
- Merge: element-wise maximum across the two vectors.
c′[i]=max(c1[i],c2[i])
- Read: sum across all slots.
total=∑ic[i]
- Because each slot only ever grows for its own replica, taking the max per slot can never lose an increment either side already recorded.
OR-Set (observed-remove set)
- State: a set of (element, unique tag) pairs, split into an add-set and a remove-set of tags.
- Add(e): mint a fresh tag, insert (e, tag) into the add-set.
- Remove(e): copy every tag currently observed for e into the remove-set; it removes only tags this replica has actually seen, never tags added elsewhere that haven't arrived yet.
- Merge: union the add-sets, union the remove-sets.
- An element counts as present if it has at least one tag in the add-set that is not in the remove-set.
Naming the comparators explicitly
| Strategy | How a conflict is resolved | What it guarantees | Where it fails |
|---|---|---|---|
| Last-write-wins (LWW) | Keep the value with the later timestamp, discard the other | Deterministic if clocks are totally ordered | Silently discards a concurrent write; a clock-skewed node can win even though its update happened earlier in real time |
| Vector clocks | Compare vectors to detect that two writes are concurrent | Tells you a conflict exists | Detection only. It does not resolve the conflict; an application or a person still has to pick a winner |
| CRDTs (this answer) | The merge function is commutative, associative, and idempotent by construction | Automatic, coordination-free convergence | Only works for data types whose semantics fit that mold; does not generalize to arbitrary business logic |
| Application-specific merge | Domain code decides, for example union two shopping carts, or keep the higher of two account balances | Correctness tailored to the domain | Bespoke code per data type; nothing about it is automatic or reusable |
Worked example: G-Counter convergence
Three replicas A, B, C start at (0,0,0):
- Replica A processes 2 local increments: its state becomes (2,0,0).
- Replica B processes 3 local increments, concurrently, before hearing from A: (0,3,0).
- Replica C stays idle: (0,0,0).
A and B exchange state and merge (element-wise max): merge((2,0,0),(0,3,0)) = (2,3,0). Read = 2+3+0 = 5. C later merges with that result: merge((0,0,0),(2,3,0)) = (2,3,0). Read = 5. Whichever order the three replicas merge in, the final vector is (2,3,0) and the read is 5, exactly matching the 2+3=5 real increments actually performed. No increment is lost and none is double-counted.
Worked example: OR-Set add and remove race
Replicas R1 and R2 have already converged on a set containing 'milk' with tag t1. The two replicas are then partitioned from each other:
- R1's user removes 'milk': remove-set gains {t1}, the only tag R1 has ever observed for 'milk'.
- R2's user, unaware of the removal, re-adds 'milk': add-set gains a brand-new tag {t2}, so the add-set is now {t1, t2}.
On merge: add-set = {t1, t2} (union), remove-set = {t1} (union). 'milk' is present because t2 is in the add-set and not in the remove-set. This is the correct outcome: R2's re-add introduced a tag the remover never saw, so it survives, exactly the observed-remove semantics the name describes.
Trade-offs & pitfalls
- Storage and bandwidth: every element needs extra metadata (a vector slot per replica for counters, a unique tag per add for sets), and removed elements don't disappear until a garbage-collection pass establishes causal stability across replicas.
- The edge case that catches teams out: CRDTs don't compose across non-commutative operations. A G-Counter or OR-Set is safe because the operations that define it (increment, tagged add and remove) are commutative by construction. But if you build an append-only log CRDT and then bolt on an application-level 'delete the last 3 entries' operation defined by position, that composition is not well-defined under concurrency: 'last 3' means something different on each replica depending on how many entries have been concurrently appended there at the time the delete runs, so two replicas can end up deleting different entries even though each individually applied a correct-looking CRDT merge. The fix is the principle OR-Set already uses: target deletions by a stable element identifier, never by position or count.
- When to avoid: anywhere a global invariant spans multiple keys (uniqueness, a balance that must never go negative), or the business logic genuinely isn't commutative. CRDTs solve convergence, not arbitrary correctness.
Given an array containing only three distinct values, sort it in-place in a single pass using constant extra space (the Dutch National Flag problem). Then explain how the same three-way in-place partitioning idea generalizes to partitioning a list by an arbitrary predicate while keeping relative order stable.
Sample Answer
Direct answer
Sort the three-valued array in one pass with three pointers, low, mid, high, that carve it into a growing "already-placed-0s" region, a "confirmed-1s" region, and a "not-yet-placed-2s" region, swapping at most once per position and using no extra storage (the Dutch National Flag algorithm). The same low/high swap idea generalizes directly to a two-way in-place partition by any predicate, but that direct generalization is not stable: swapping elements out of place can scramble the relative order of items that land in the same output region, so keeping order stable needs an extra step beyond just relabeling the predicate.
Structured elaboration
The three-way invariant
At every point during the scan: everything before low is 0, everything in [low, mid) is 1, everything after high is 2, and mid is the next unclassified element. When nums[mid] == 0, swapping it to low and advancing both pointers is safe because the element that lands at mid from the swap was already known to be 1 (it came from inside [low, mid)). When nums[mid] == 2, swapping it to high and only decrementing high (not advancing mid) is required because the element swapped in from the unexplored tail is unclassified and must still be examined.
Generalizing to an arbitrary predicate, and where stability breaks
Collapsing the three categories to two, "satisfies the predicate" versus "does not", turns this into the standard in-place two-way partition: scan with a single pointer, swap anything satisfying the predicate to the front. This preserves the O(n) time, O(1) space profile, but it is not stable: a swap moves an element across positions that may already hold other same-category elements, and nothing about the swap preserves their relative order. The demonstration below shows two items in the same category (both satisfying the predicate) ending up reordered relative to each other purely because of how the swaps landed, even though the partition itself (which items are in which category) is correct.
To keep relative order stable while partitioning in place, the pointer-swap approach is not enough on its own. Two honest options:
- Trade space for stability (simple): scan once, appending predicate-true and predicate-false items to two separate output lists in encounter order, then concatenate. This is O(n) time and stable by construction, at the cost of O(n) auxiliary space, no longer in-place.
- Keep O(1) space, pay in time (advanced): a genuinely in-place and stable partition is possible using a recursive divide-and-rotate scheme, partition each half of the array independently, then merge the two halves' boundary regions by rotating the "false" suffix of the left half past the "true" prefix of the right half. This is the same technique behind library-grade in-place stable partitions (for example, C++'s
std::stable_partitionis specified to do at most O(nlogn) swaps in the general case, or a single linear pass if it is allowed to allocate a temporary buffer). It is a materially different algorithm from the plain DNF swap, not just the same three-pointer idea relabeled.
Worked example
Approach
First, the DNF sort itself:
def dutch_national_flag(nums):
low, mid, high = 0, 0, len(nums) - 1
while mid <= high:
if nums[mid] == 0:
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1
elif nums[mid] == 1:
mid += 1
else: # nums[mid] == 2
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
return nums
print(dutch_national_flag([2, 0, 2, 1, 1, 0]))
This prints [0, 0, 1, 1, 2, 2].
Second, the two-way generalization by predicate, showing the instability directly on tagged items so reordering is visible:
def unstable_partition(items, pred):
lo = 0
for i in range(len(items)):
if pred(items[i]):
items[lo], items[i] = items[i], items[lo]
lo += 1
return items
labeled = [('a', 1), ('b', 0), ('c', 1), ('d', 0), ('e', 1)]
print(unstable_partition(labeled[:], lambda t: t[1] == 1))
def stable_partition(items, pred):
true_bucket = [x for x in items if pred(x)]
false_bucket = [x for x in items if not pred(x)]
return true_bucket + false_bucket
print(stable_partition(labeled[:], lambda t: t[1] == 1))
This prints:
[('a', 1), ('c', 1), ('e', 1), ('d', 0), ('b', 0)]
[('a', 1), ('c', 1), ('e', 1), ('b', 0), ('d', 0)]
The swap-based version reorders 'b' and 'd' relative to each other (both are category-0, and 'd' ends up before 'b', the reverse of their original order), while the extra-buffer version preserves 'b' before 'd' exactly as encountered.
Key points
- The DNF three-pointer scan and the two-way predicate partition are the same swap mechanism; only the number of categories changes.
- Correctness of which bucket each item lands in is unaffected by the instability; only the order within a bucket is at risk.
- Stability and true in-place (O(1) space) are in tension for this problem: you can have both only by accepting O(nlogn) time via the rotation-based approach, or you can have O(n) time by giving up in-place-ness.
Complexity
DNF: time O(n) (single pass, mid never revisits a position), space O(1).
Unstable two-way partition: time O(n), space O(1).
Stable partition via extra buffer: time O(n), space O(n).
Edge cases
- Empty array, single element, or an already-sorted array: the DNF loop terminates immediately or after trivial no-op passes.
- All elements identical (all
1s, for instance):lownever advances,midsweeps straight tohighwith no swaps. - A predicate that is trivially true or false for every element: the two-way partition degenerates to a no-op copy.
Trade-offs & pitfalls
The most common mistake when moving from three-way DNF to a two-way predicate partition is assuming the result is automatically stable "because it's the same kind of algorithm." It is not, and that distinction matters directly whenever a partition needs to preserve, for example, insertion order or timestamp order within each output group (a very common real requirement, such as partitioning a task queue into "ready" versus "blocked" while keeping each group's original ordering). Reaching for the swap-based version there without checking the stability requirement is a silent correctness bug, not just a style choice. When stability genuinely is not required, the swap-based version remains the better default: no allocation, single pass, and it generalizes cleanly to k-way partitioning by widening from two pointers to k−1 boundary pointers.
Describe the role of assertions and invariants in maintaining code correctness. When should assertions be used versus throwing exceptions? Provide an example where an assertion detects a developer error early and avoids a costly runtime check in production, and describe how this maps to design-by-contract thinking (preconditions, postconditions, invariants).
Sample Answer
Direct answer
An assertion checks a condition that your own code's logic guarantees should always be true if nothing upstream has a bug; an exception handles a condition that the outside world (a user, a file system, a network) can legitimately produce regardless of whether your code is correct. Assertions catch developer errors early and cheaply; exceptions handle the world being unpredictable.
Structured elaboration
What an assertion is for. An assertion encodes an invariant: a statement that, given correct code, must hold at this point in the program no matter what valid input arrives. If it fails, the bug is in the code that led to this point, not in the input or the environment. Because of this, assertions are cheap to reason about (you never need a recovery path for them, the correct response to a failed assertion is to fix the bug) and, in several languages, can be compiled out entirely in optimized production builds, which is precisely why they must never be relied on for something the outside world can trigger.
What an exception is for. An exception handles a condition that is a normal, expected possibility given a correct program: a file that does not exist, a network call that times out, a user who submits invalid input. These require an actual recovery path (retry, a default, informing the user) because they will happen in production no matter how correct the code is.
Preconditions, postconditions, and invariants (design by contract). A precondition is what a function requires to be true of its inputs to behave correctly; a postcondition is what it guarantees to be true of its output if the precondition held; an invariant is a condition that must hold at every observable point in an object's lifetime. Assertions are the natural implementation mechanism for all three inside a single codebase's own internal logic: asserting a precondition at function entry catches a caller who violated the contract due to a bug in their own code, which is different from validating an input that arrived from outside the trust boundary and might be malformed for entirely legitimate reasons.
The line between the two. The test is not "is this input bad", it's "could this input be bad even if every line of my own code is correct". A negative array length passed internally between two functions you wrote and control, where nothing external can produce that value if your code is right, is an assertion case. A negative quantity field parsed from a JSON request body is an exception (or validation-error) case, because a malicious or buggy client can produce it no matter how correct your server code is.
Worked example
A function withdraw(account, amount) internal to a ledger system might assert assert account.balance >= 0, "invariant violated: account balance went negative" right after debiting, because if the debit logic is correct, the balance should never go negative; if this assertion fires, there's a bug in the debit logic itself, and the fix is to find that bug, not to add a check that reacts gracefully to a negative balance in production. Contrast this with the same function's very first line, which must instead RAISE an exception (not assert) if the caller passes a negative amount: a negative withdrawal amount is exactly the kind of thing an upstream caller (a request handler parsing user input) can produce, whether or not the ledger's own internal logic has any bugs at all, so it needs a real, always-active check, and the right response is to reject the request, not to crash a debug build's assertion and silently no-op in production.
Trade-offs and pitfalls
The most costly mistake is using an assertion to validate something a real caller can trigger: because assertions can be disabled in optimized builds in several languages (C's NDEBUG, Python's -O flag), a check that only exists as an assertion can silently vanish in production, meaning the exact case it was meant to guard against reaches production code entirely unchecked. The opposite mistake, wrapping every internal invariant in a full exception with a try/catch elsewhere in the codebase, adds real performance and readability cost for something that, if your own code is correct, should genuinely never happen and does not need a recovery path at all, only a way to fail loudly and immediately during development and testing.
A product manager, designer, and engineering team all want different things for the same release. How would you facilitate alignment, surface the trade-offs, and decide what ships first without damaging the working relationship?
Sample Answer
I’d facilitate the conversation around the shared objective first, because people usually disagree on solutions, not the user problem.
My approach:
- Restate the goal and the decision we need to make.
- Ask each function to explain what they need and why.
- Separate must-haves from preferences.
- Use clear criteria: user impact, effort, risk, and release timing.
Then I’d surface the trade-offs openly: if we choose the designer’s version, what slips? If we choose engineering’s approach, what user value do we lose? That makes the decision concrete instead of political.
If the team still can’t align, I’d make the call based on the agreed criteria and explain the rationale. I’d also make sure the decision is documented so nobody feels blindsided later.
What matters most is tone: I’d be firm on the decision but respectful of every viewpoint. People can disagree and still feel heard, which protects the working relationship after the release.
Worked example
Say the release in question is an onboarding redesign: the designer wants a fully polished new flow with custom illustrations and micro-interactions, while engineering proposes a simplified version that reuses existing components to hit the release date. Scoring both against the agreed criteria (user impact, effort, risk, release timing) shows the simplified version delivers most of the user-impact gain at a fraction of the effort and with no timeline risk, while the fully polished version would slip the release by three weeks for a comparatively small additional lift in user impact. So the simplified version ships first, and the custom illustrations and micro-interactions move into a fast-follow scoped for the next release, which is the trade-off made concrete instead of staying a hypothetical "what if."
You're deploying a fraud-detection model that scores card transactions. Would you serve it as a synchronous call inside the authorization path, or run it as a scheduled batch job? Walk me through what drives that choice and what would flip your answer.
Sample Answer
Direct answer
Use synchronous scoring only when the action on the score must happen before the transaction completes, such as blocking a fraudulent charge. If the action can wait (nightly review, retraining labels), batch scoring buys a heavier, more accurate model at a fraction of the cost. The deciding question: what does a delayed decision cost you, versus an always-on low-latency fleet?
Structured elaboration
Real-time: one hop in a hard end-to-end latency budget, needs near-zero-staleness features (a hot feature store: a system that serves the same precomputed input values, like a rolling transaction count, to the model at scoring time; "hot" means it's updated in near real time rather than nightly), and an always-on fleet sized for peak plus a fallback if the call times out.
Batch: runs on a schedule, so it can use a bigger, slower model on cheaper bursty compute, but adds a detection lag equal to the batch interval, and a partial failure is a silent under-score, not an outage.
Worked example
A 250ms authorize/decline budget, with auth, ledger, and notification already at 180ms:
fraud model budget=250ms−180ms=70ms
Subtract 20ms network/serialization overhead:
compute budget=70ms−20ms=50ms
That rules out heavy ensembles (combining predictions from several models, which multiplies the per-request compute cost) needing multiple feature joins (a feature store lookup that assembles several separate precomputed values into one input record, each join adding its own latency): illustratively, a 3-model ensemble with 2 extra feature joins might cost 90-120ms on its own, already over the 50ms budget, which is what "rules out" means here concretely. For batch: 10 million transactions overnight, 500 records per inference batch, 50ms per batch on one GPU:
batches=50010,000,000=20,000,GPU time=20,000×50ms≈16.7 min
Across 4 GPUs, about 4 minutes wall time, no idle fleet cost during the day.
Trade-offs and pitfalls
Real-time buys speed but pays for peak capacity around the clock and widens the request's failure surface. Batch is cheaper but the fraud can complete before you act. A common mistake is defaulting to real-time without pricing the fleet against the actual cost of delay. The answer flips when either side of that equation moves: if fraud losses from a delayed decision start to dwarf the cost of an always-on fleet, or a cheaper real-time model becomes accurate enough to fit the leftover latency budget, you're pushed toward real-time; if the model needs more compute than the authorization path can spare, or false declines against legitimate customers become the bigger cost, you're pushed back toward batch.
What the interviewer probes next
Hybrid streaming or micro-batch designs, detecting a batch job that fails partway through, and how the answer shifts if a missed fraud case gets an order of magnitude costlier.
Recommended Additional Resources
- LeetCode (focus on Meta-tagged medium difficulty problems)
- System Design Interview by Alex Xu (book for system design fundamentals)
- Designing Data-Intensive Applications by Martin Kleppmann (advanced system design concepts)
- Meta Careers official site: metacareers.com (official job postings and company information)
- Blind (anonymous engineer discussions about Meta interview experiences)
- Levels.fyi (Meta compensation and interview process insights)
- YouTube: Search 'Meta Software Engineer Interview Experience' for real candidate walkthroughs
- Pramp (free mock interview platform for coding and system design practice)
- InterviewBit (curated coding interview problems with solutions)
- System Design Primer GitHub repository (comprehensive distributed systems resource)
Search Results
Proven Meta Software Engineer interview guide (2025) | Prepfully
The Meta Software Engineer interview has 3 rounds: Recruiter Phone Screen, Technical Phone Screen, and Onsite Round. The onsite round includes technical, ...
Meta Interviews 2025: Questions, Process, and Prep Playbook
Expect three to five interviews across a single day or split over two. For technical roles, this might include system design and product sense ...
Meta Interview Experience 2025 | Software Engineer - YouTube
... Interview Process 2025 | Backend Engineer - https://youtu.be/pqdp7_ZKYKk Stock Trading App System Design Interview | Meta System Design ...
Meta Software Engineer Interview (questions, process, prep)
What's the Meta interview process and timeline for the software engineer role? It takes four to eight weeks on average and follows these steps:.
Preparing for Your Full Loop Interview at Meta - Meta Careers
The full loop interview is designed to assess your technical skills, help hiring managers get to know you and give you insight into the opportunities to build ...
Meta Software Engineer Interview Experience - United States - Taro
Meta's interview process for their Software Engineer roles in the United States is extremely selective, failing the vast majority of engineers.
Meta Data Engineer 2025 Interview Experience | Tech Industry - Blind
5 signals they're looking, expect one to two question for each. Prepare stories for each scenario with impact and quantifiable metrics.
This interview preparation guide was generated using AI-powered research from the sources listed above. While we strive for accuracy, we recommend verifying critical information from official company sources.
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 Software Engineer jobs
AI-enriched listings across hundreds of company career pages
Explore Jobs