Interview guides · Applovin
Applovin Software Engineer Interview Guide
Applovin software engineer prep on operations: decide what you would need to see to know a release is going wrong.
The 60-second brief
As a Software Engineer at Applovin, you are at the core of a high-scale, profitable ecosystem that powers mobile game advertising for billions of end-users. You will work within an engineering organization of approximately 200 people, building robust backend systems that handle massive throughput, real-time bidding, and complex data processing. Whether you are on the backend team working with Java and C++ or the data team leveraging Scala and Spark, your code directly influences the company's ability to serve 14 billion end-users globally.
The title spans product, platform and infrastructure work, and which of those the seat actually is decides whether design or algorithms carries more weight in your preparation. The posting rarely settles it; what the team is on call for usually does.
Focus areas
- Restate attribution results without double-counting credit
- Make impression billing idempotent under at-least-once delivery
- Select candidate line items without scanning them all
Overview
This role is for engineers who thrive on technical depth and high-impact problem solving. You will be expected to design systems that are not just functional, but highly optimized for performance and scalability. Because Applovin operates in a fast-paced, data-driven environment, you will have the opportunity to influence critical infrastructure, optimize ad-delivery pipelines, and solve complex challenges that directly impact the company’s bottom line.
The engineering culture at Applovin values pragmatism. You will be expected to demonstrate not just the ability to write code, but the ability to write production-ready, efficient, and maintainable software under time constraints.
The Applovin interview process
Applovin candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Call
The person on this call usually cannot evaluate your code and does not need to. They write a short paragraph, and that paragraph is what a hiring manager skims when deciding who to put on your loop. So the test is not whether your work was hard, it is whether a non-engineer can repeat it correctly. Name systems by what they did rather than by their internal codename, give each project a shape (what was breaking, what you changed, what happened after), and keep the whole walkthrough near ninety seconds. Depth that cannot survive a paraphrase reads as vagueness.
What to demonstrate
- Whether a non-engineer can restate your projects without distorting them, since their paraphrase is what travels to the hiring manager, not your sentences
- Whether each project has a shape rather than a stack list: the failure or constraint, the change you made, the result and how it was measured
- Whether you can say what was yours inside a team project without either inflating it or disappearing into the plural
How to prepare
- Rewrite each headline project as two sentences with no internal system names and no acronyms outside your company, then say them to someone outside engineering and have them repeat them back. Fix whatever came back wrong
- Attach one measured number to each project: the baseline, the change, and the window it was measured over. Where nothing was ever measured, say that plainly rather than reaching for a plausible percentage
- Time the background walkthrough against a clock. If it runs past two minutes, compress the earliest role to a single clause and spend the recovered time on the most recent one
Technical Phone Screens
Before anything technical happens, someone has to decide which rung of the ladder your loop is calibrated to, and that decision sets the bar for every round after it. It comes from how you describe scope, not from your title, because titles do not convert cleanly between companies. The weak version of the answer is team size and years. The strong version names the largest change you shipped where nobody reviewed the design, what would have broken if you had been wrong, and what you were paged for. Get the level said out loud on this call, because the range and the loop both follow from it.
What to demonstrate
- Whether the scope in your own account maps onto a level the team actually has an opening at, so a mismatch ends the process cheaply rather than after four interviewers have spent a day
- Whether your title needs re-mapping: the same word describes very different amounts of independent decision-making at a twenty-person company and a ten-thousand-person one
- Whether your compensation expectation can be filled at that level in the structure the role pays in, which is why the number gets asked for before any engineer is scheduled
How to prepare
- Write down two changes from the last two years: the largest one you designed with nobody reviewing the design, and the largest one where someone more senior did. Lead with the first when scope comes up, and be ready to say which parts of the second were yours
- Ask which level the loop is calibrated to and what changes at the level above it, then plan your weeks from that answer rather than from the posting
- Settle a total-compensation range beforehand with the split named, base against bonus against equity and its vesting period, so a question about numbers gets a number instead of the word market
Virtual Onsite
Coding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.
What to demonstrate
- Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
- Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
- Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
- Whether you can say which calls you made alone and which you escalated, and why the line sat where it did
How to prepare
- Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
- Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
- Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap
Where candidates lose points
Selecting candidate line items by scanning active line items and evaluating each one's targeting predicates.
It is correct, it is trivially testable, and its cost grows linearly with the number of line items on a path that has tens of milliseconds total, so it works in staging and degrades as the business grows rather than as traffic grows, which disguises the cause. The structure that fits is an inverted index over predicate values where a conjunctive match is found by intersecting posting lists and requiring the matched conjunct count to equal the line item's conjunct count, making the cost scale with predicate values on the request instead. The second half of the trap is forgetting that one bid request carries several impression slots, so the fan-out is per slot and a per-request budget is not a per-match budget.
Deduplicating conversions on the advertiser-supplied event identifier alone.
That identifier is often an order number, which is unique within one advertiser's system and not across advertisers, so a global unique key silently drops one advertiser's conversions when another has already used the value. Scoping the key to the advertiser fixes the collision but creates the opposite error if source is also collapsed into it: advertisers deliberately fire both a browser pixel and a server-side call for the same purchase so that one covers for the other, and a key that cannot represent both arrivals throws away the evidence needed to decide which to keep. Deduplicate structurally on advertiser, source and key, then resolve across sources as an explicit step with its own recorded outcome.
Retrying a write that is not safe to repeat
A timeout tells you nothing about whether the server applied the write, so a blind retry of a create or a charge can duplicate it. Either make the operation idempotent, with a caller-supplied key the server deduplicates on or a conditional update, or do not retry it; and use exponential backoff with jitter so the retries of many clients do not synchronise into a second outage.
Reading the constraints as preamble rather than as part of the problem
The bounds are usually there to eliminate the obvious approach: n up to 10^5 makes an O(n^2) scan roughly 10^10 operations, far outside any per-test time budget, and an input larger than memory rules out loading it at all. When a bound is not given, ask for it, then say out loud which approach it kills.
Questions & practice
Implement a Fibonacci function recursively with optimization.
Medium · data structures and algorithms
Approach
- State the target complexity and say which constraint rules the naive version out.
- Choose the data structure from the access pattern, not from familiarity.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- How does this change if the input no longer fits in memory?
- What is the worst case, and how likely is it on real data?
Group anagrams from a list of strings.
Medium · data structures and algorithms
Approach
- State the target complexity and say which constraint rules the naive version out.
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Find the shortest path in a grid using DFS/BFS.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
Count nodes in a Binary Search Tree that fail specific rules.
Medium · data structures and algorithms
Approach
- Restate the input: its shape, its size, and what is guaranteed about it.
- Name the brute-force solution and its complexity before improving on it.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
What is the difference between malloc and new in C++?
Medium · languages, concurrency and fundamentals
Approach
- Say what the runtime actually does before reasoning about the code.
- Name what is shared across threads and what owns each piece of state.
- Distinguish a value from a reference to it, and say which one you handed out.
Follow-up
- What happens if two callers reach this at the same time?
- How would you prove the race exists rather than suspect it?
What is the difference between start and run in a Thread?
Medium · languages, concurrency and fundamentals
Approach
- Name what is shared across threads and what owns each piece of state.
- Reach for the cheapest primitive that closes the race, not the broadest lock.
- Say what the runtime actually does before reasoning about the code.
Follow-up
- What happens if two callers reach this at the same time?
- How would you prove the race exists rather than suspect it?
Replace a candidate scan with conjunctive posting list intersection
Hard · inverted index · set intersection · candidate retrieval
Candidate selection currently loops over every active line item and evaluates its targeting predicates against the request. It is correct and it passes every test. With 10^6 active line items, a request carrying about 40 predicate values across 6 impression slots, and tens of milliseconds for the whole request, it no longer fits. Design the replacement. Each line item's targeting is a conjunction of clauses, and a clause may itself be a set membership such as geo in {A, B, C}. Handle negative targeting, and state both complexities.
Approach
- Name why the scan degrades the way it does, because that framing is half the question: its cost is O(L x clauses) in the number of line items, so it grows with the business rather than with traffic. It passes a load test and fails a year later with no change in request volume, which disguises the cause.
- Build an inverted index from predicate value to a posting list of (line_item_id, clause_id). At request time gather the posting lists for the roughly 40 values the request carries and accumulate per candidate, which makes the cost O(sum of posting-list lengths + distinct candidates touched) - it scales with request predicates, not with L.
- Match conjunctions by counting satisfied clause ids distinctly, not posting-list hits. A clause like segment in {s1, s2} puts the line item on two posting lists, so a request carrying both values would credit one clause twice and let the line item reach its required count with another clause genuinely unsatisfied. A per-candidate 64-bit mask of satisfied clause ids, matched by popcount against the required count, fixes it, with a slow path for the rare line item above 64 clauses.
- Handle negation with a separate exclusion index, because absence proves nothing: a line item excluding a value is simply not on that value's inclusion list, which is indistinguishable from indifference. Accumulate exclusion hits in the same pass and drop any candidate whose exclusion mask is non-zero.
- Handle a missing attribute explicitly: a request carrying no geo must not match a line item that requires one, so the required clause count comes from the line item, and a clause the request cannot satisfy simply never increments.
- Account for the fan-out: this runs per impression slot against a budget that is per request. Compute the request-level predicate values once and re-run only the clauses that differ per slot, which is typically format and size.
Worked solution (40 min)
- Write the scan's cost in terms of L and clause count, and state the growth axis explicitly: line items, not queries per second.
- Construct three line items, one of them with a two-value set-membership clause, and hand-run the accumulation for a request carrying both values to expose the double-count.
- Replace the counter with a clause-id bitmask and re-run the same example to show the line item now correctly fails to match.
- Add one line item with a negative clause and show the separate exclusion pass dropping it.
- State the final complexity and the per-slot fan-out against the per-request budget.
Expected result: An inverted index keyed on predicate value holding (line_item_id, clause_id) postings, with per-candidate clause bitmasks matched by popcount against each line item's required clause count, a separate exclusion index consulted in the same pass, and a per-slot cost of O(sum of posting-list lengths + candidates touched) rather than O(L).
Verify it yourself
- A line item whose set-membership clause matches two request values still requires every one of its other clauses to match.
- A request missing an attribute entirely never matches a line item that requires that attribute.
- Over a randomly generated corpus the index produces exactly the candidate set the scan produces, which is the property that says this is an optimisation and not a change of semantics.
Follow-up
- Posting lists for common values such as a large country hold hundreds of thousands of entries. How do you stop the intersection degenerating back into a scan?
- How is the index rebuilt and shipped, and what staleness bound can a bidder state about its copy?
- A line item is paused. Where does it stop matching, and how quickly?
Add a backfilled NOT NULL column to a live partitioned table
Hard · online migration · locking · partitioning
delivery_event is RANGE partitioned by received_ts date in PostgreSQL 14, absorbs order 10^5 inserts per second, and holds billions of rows across live partitions. You must add supply_source_id INT NOT NULL, whose value is derived per row from exchange_id through a mapping table, and index it for reporting. Write the ordered migration plan: where each lock is taken and what it blocks, how writers and the backfill coexist, how the column becomes NOT NULL, and how the index is built. State which steps rewrite data and which do not.
Approach
- Sequence the deploys, not just the DDL: add the column nullable, ship writer code that populates it on every insert, backfill the history, then tighten to NOT NULL. Any other order either breaks inserts the moment the constraint lands or leaves a growing tail of rows the backfill has already passed.
- Be precise about which step rewrites. From PostgreSQL 11, ADD COLUMN with no default or with a non-volatile default does not rewrite the table; the default is stored in the catalog and materialised on read. A value derived per row from a mapping table is not a default, so nullable-then-backfill is forced rather than chosen.
- Account for the lock even when the work is instant. The catalog change takes ACCESS EXCLUSIVE on the partitioned parent and recurses to every partition, and PostgreSQL queues lock requests, so a pending ACCESS EXCLUSIVE also blocks every reader and writer that arrives behind it. One long analytics query turns a millisecond DDL into a multi-minute stall. Set lock_timeout to a couple of seconds and retry the statement in a loop.
- Backfill per partition in bounded key ranges with a commit per batch, throttled on replication lag. A single UPDATE over billions of rows holds one snapshot open for hours, which blocks vacuum from reclaiming dead tuples across the database, writes one enormous WAL burst, and has to start over if it fails.
- Take NOT NULL without the full scan: ADD CONSTRAINT ... CHECK (supply_source_id IS NOT NULL) NOT VALID, which is a brief catalog-only lock, then VALIDATE CONSTRAINT, which takes only SHARE UPDATE EXCLUSIVE and does not block reads or writes, then SET NOT NULL, whose scan PostgreSQL 12 and later skips when a valid equivalent CHECK already exists, then drop the CHECK.
- Build the index per partition, because CREATE INDEX CONCURRENTLY is not permitted on a partitioned parent. Create the parent index ON ONLY, which leaves it invalid, build each partition's index CONCURRENTLY, then ALTER INDEX ... ATTACH PARTITION each one; the parent index becomes valid once the last partition is attached.
Worked solution (45 min)
- Write the plan as a numbered deploy sequence with the application deploys interleaved between the DDL steps, not as one migration script.
- Annotate each statement with the lock mode it acquires, what that lock blocks, and whether the step reads rows.
- Write the backfill loop with its batch key, batch size, commit point and the lag signal that throttles it.
- Write the NOT NULL sequence as CHECK NOT VALID, VALIDATE CONSTRAINT, SET NOT NULL, DROP CONSTRAINT, marking which of the four scans data.
- Write the index creation for a three-partition example including the ON ONLY parent index and the ATTACH PARTITION steps.
Expected result: A sequence in which no single step both holds ACCESS EXCLUSIVE and scans rows: column added nullable, writers deployed, batched throttled backfill, CHECK NOT VALID then VALIDATE then SET NOT NULL then DROP, per-partition CREATE INDEX CONCURRENTLY then ATTACH, with every ACCESS EXCLUSIVE step wrapped in lock_timeout and retry.
Verify it yourself
- No step in the plan holds ACCESS EXCLUSIVE while reading rows.
- Inserts succeed at every point in the sequence, including between the column appearing and the backfill finishing.
- Each behaviour claimed is tied to the PostgreSQL version that provides it.
- A crash partway through the backfill resumes from the last committed range rather than rescanning completed ones.
Follow-up
- The backfill has to cover the partition currently taking 10^5 inserts per second. What do you do differently there compared with a sealed partition?
- At that insert rate a daily partition holds billions of rows. Is date the right partition granularity, and what breaks if you change it now?
- CREATE INDEX CONCURRENTLY fails on one partition. What is the state of that index, what does it cost you while it sits there, and what do you run next?
Model attribution restatement so exactly one revision stays live
Hard · supersession · audit history · constraints
attribution(attribution_id, conversion_id, impression_id, line_item_id, model_id, touchpoint_type, credit_fraction, window_days, revision, is_live, superseded_by_attribution_id, computed_ts) is recomputed whenever a late touchpoint or late conversion changes an already-published result. A multi-touch model writes several rows per conversion, so several rows are live at once, but all live rows for one (conversion_id, model_id) must share a single revision and their credit_fraction must sum to 1. Give the PostgreSQL DDL that enforces what is enforceable, name what no constraint can enforce here, and write the transaction that publishes revision r+1.
Approach
- Separate the three claims by shape, because they need three different mechanisms. At most one live row per (conversion_id, impression_id, model_id) is a uniqueness property. All live rows sharing one revision is a property of a group of rows. Fractions summing to 1 is an aggregate predicate over that group.
- Make the revision itself a row: attribution_revision(conversion_id, model_id, revision, computed_ts, window_days) with PRIMARY KEY (conversion_id, model_id, revision). Detail rows carry that triple with a foreign key to it and UNIQUE (conversion_id, model_id, revision, impression_id). is_live and superseded_by_attribution_id then leave the detail row entirely.
- Make liveness one row: attribution_live(conversion_id, model_id, live_revision) with PRIMARY KEY (conversion_id, model_id) and a foreign key on (conversion_id, model_id, live_revision) into attribution_revision. Exactly-one-live is now that primary key, publishing is a single upsert, and no detail row is ever updated.
- Say why the boolean on the detail row is worse than it looks. A partial unique index on (conversion_id, model_id) WHERE is_live forbids the second touchpoint of any multi-touch model, and flipping the flag across two revisions in one statement can transiently violate the index, which cannot be deferred in PostgreSQL because deferrable uniqueness requires a constraint and unique constraints cannot carry a WHERE clause.
- Concede the sum-to-one invariant honestly: PostgreSQL has no CREATE ASSERTION and a CHECK constraint sees a single row, so this one is not enforceable declaratively. Implement it as a constraint trigger deferred to commit, or as a gate query inside the publish transaction, and say which.
- Write the publish transaction in order: insert the attribution_revision row, insert the detail rows for r+1, run the sum gate over exactly those rows, then upsert attribution_live. A failed gate aborts before the pointer moves, leaving revision r live and the rejected rows retrievable as history rather than lost.
Follow-up
- One conversion is restated hourly, so its attribution_live row is a write hot spot. What does that actually cost, and at what restatement rate does it start to matter?
- A report starts before the pointer flips and finishes after. Which revision does its output contain, and is a report that straddles a restatement acceptable or does it need a snapshot?
- You must delete revisions older than 90 days. How do you avoid dropping a revision that an issued invoice cited?
Design a ranked list system supporting efficient insertion and rank retrieval.
Medium · system design and architecture
Approach
- Choose a partition key and say what query it makes expensive.
- Name the read and write paths separately; they rarely have the same bottleneck.
- State the consistency you need, and where you are willing to be stale.
Follow-up
- What would you drop to keep the system up under load?
- How does this behave when that dependency is down for an hour?
Design a ride-sharing system similar to Uber.
Medium · system design and architecture
Approach
- State the consistency you need, and where you are willing to be stale.
- Choose a partition key and say what query it makes expensive.
- Name the read and write paths separately; they rarely have the same bottleneck.
Follow-up
- What would you drop to keep the system up under load?
- How does this behave when that dependency is down for an hour?
Rate limit a reporting API without causing retry storms
Medium · rate-limiting · backpressure · http-semantics
Your public reporting API serves clients ranging from a dashboard polling every thirty seconds to a partner running a nightly export with forty parallel workers under one credential. Read queries differ in cost by two orders of magnitude: a line-item status read against a ninety-day facet aggregation. The same clients already retry automatically on 5xx. Design the limiting scheme: the algorithm, what the limit is keyed on, the response contract when a caller exceeds it, and how it composes with those existing retries.
Approach
- Choose a token bucket or GCRA over a fixed window. A fixed window admits up to twice the nominal limit across a boundary — the full allowance at the end of one window and again at the start of the next — and its reset instant synchronises every blocked client into a thundering herd. A bucket with refill rate r and burst B lets you state sustained rate and burst independently and enforces smoothly in between.
- Key on two levels, credential and advertiser account, so one runaway integration cannot consume the whole account's allowance, and charge cost-weighted tokens rather than one per request. Publish the cost of each endpoint class and tie it to work actually done — rows scanned, partitions touched — so the weighting survives contact with a caller who wants to argue about it.
- Reject before doing the work, and make the rejection informative: 429 with Retry-After, plus pacing headers from the RateLimit-* family (still an IETF draft, so pin the exact spelling you emit in your documentation) so a well-behaved client can stay under the limit rather than discovering it by being refused.
- Make rejection compose with the retries clients already have. A 429 must consume no idempotency key and apply no partial write, so retrying is genuinely free; it must be distinguishable in your error taxonomy from 503, because the correct backoff differs; and your client guidance must require jitter, since the failure mode you are designing against is synchronised retry rather than raw volume.
- Separate a concurrency limit from the rate limit. Forty parallel workers under one credential are a concurrency problem: a per-second rate does not stop forty simultaneous expensive aggregations from saturating the query path between refills. Cap in-flight expensive requests per credential and queue or reject beyond it, with the queue bounded so waiting cannot exceed the client's own timeout.
Worked solution (25 min)
- Write bucket parameters for the two client profiles and describe what each does at steady state and at the moment of a burst.
- Assign a token cost per endpoint class and justify the ratio by the work done, not by intuition about importance.
- Work the fixed-window boundary numerically — the full limit consumed at the end of one window and again at the start of the next — and show what the bucket admits over the same interval.
- Write the 429 response with Retry-After and the pacing headers, then write the client backoff rule and verify that two clients refused at the same instant do not retry at the same instant.
Expected result: A cost-weighted token bucket keyed on credential and on account, a separate bounded concurrency limit for expensive reads, a 429 carrying Retry-After and pacing headers, and documented client backoff with jitter that treats 429 differently from 503.
Verify it yourself
- No request sequence admits more than burst plus rate times elapsed over any interval, including one spanning a window boundary.
- A refused request consumed no idempotency key and left no partial write.
- One credential exhausting its allowance degrades only that credential, not the account's other integrations.
Follow-up
- An internal caller on a deadline-bounded path reaches this limiter with twelve milliseconds of budget left — should it queue or be refused immediately, and what does it do with the refusal?
- How do you let the partner's nightly export run at full speed overnight without giving them that rate during the day?
Exchange reports late bids while internal dashboards look healthy
Easy · deadline propagation · tail latency · timeouts
Your bidder answers an exchange that enforces a 100 ms deadline measured from its own send to its own receive. Internal dashboards show mean handler time at 9 ms with no regression at any deploy, but the exchange now discards 6% of your responses as late and has warned about throttling your access to its supply. The bid path makes three sequential dependency calls, each given a 40 ms timeout, added by three different changes. Give the ordered list of what you inspect and in what order, and the change you make to the timeout scheme.
Approach
- Fix the measurement boundary before trusting any number. The exchange times from its socket write to its socket read, so its figure includes network round trip, your accept-queue wait, TLS handshake on cold connections and response flush. Instrument accept-to-flush and compare it against the handler-only timer; a divergence puts the regression in queueing rather than in code.
- Replace the mean with p99 and p99.9, cut per exchange and per region. At 10^5 requests per second a 6% late rate is thousands of requests a second, and a mean over that population cannot move enough to be visible. An average is the wrong statistic for a deadline because the deadline is a threshold, not a central tendency.
- Do the deadline arithmetic explicitly. Take 100 ms, subtract measured round trip and the exchange's own slack, and you have roughly 70 ms of internal budget. Three sequential calls with 40 ms timeouts admit 120 ms, so any two slow hops blow the budget while every per-dependency dashboard still reports healthy, because each hop stayed inside its own constant.
- Attribute the tail rather than the mean: per-hop span timings at p99, garbage collection and pause logs, connection pool checkout wait, and DNS or TLS cost on new connections. Look for the hop whose p99 moved, not whose average moved.
- Change the scheme to a deadline carried on the request and decremented at every hop, granting each call the remaining time minus a reserve rather than a constant. Write the degraded behaviour next to each dependency before writing code: which ones can be skipped into a lower-confidence bid and which force a no-bid. A late response is worse than a no-bid, because it consumes capacity, buys nothing, and counts against the supply relationship.
- Alert on your own internal budget overrun rate rather than waiting for the exchange to complain, since the exchange's signal arrives only after it has already decided to throttle you.
Follow-up
- The identity lookup has a p50 of 2 ms and a p99 of 36 ms. Do you keep it on the path, and what does the bid look like when you skip it?
- How do you tell a no-bid caused by a squeezed deadline apart from a no-bid caused by no eligible line item, in metrics you would actually keep?
- The exchange's deadline and your measured round trip both vary by region. Where does the per-region budget live, and who owns changing it?
Suggested practice checklist
Roughly ninety minutes on weeknights with one longer weekend block. The plan cuts scope rather than compressing everything, on the assumption that one thing finished per night beats four half-started.
Day 1 — Fix the scope and take a cold baseline
- Read the role description and write the three things the loop will almost certainly test, then write an explicit not-doing list and keep it visible all week.
- Take one twenty-five-minute coding problem and one fifteen-minute design prompt cold, and write the single sentence naming what blocked each, because those two sentences decide where the remaining evenings go.
- Set the week's rule: one thing finished every night, including the night you only have forty minutes.
Deliverable: A one-page scope with a not-doing list and two cold attempts, each carrying one sentence on what blocked it.
Day 2 — One pattern, written three times from blank
- Choose the single pattern most likely to appear in your loop and write it three times from an empty file rather than editing the previous attempt.
- On the third pass, write the invariant as a comment before the loop body and the complexity before the first line of code.
- Stop at ninety minutes even if the third version is imperfect, and write the one thing you would fix given another hour.
Deliverable: Three independent implementations of the same pattern plus a note on what changed between them.
Day 3 — One design, only to the depth you can defend
- Take one system shape and go only as far as requirements, interface and data model, refusing to draw a box you could not survive a follow-up about.
- Attach one number to each non-functional requirement, deriving it rather than asserting it, and write the assumption the number rests on.
- Write the one tradeoff you are choosing against and the observation that would make you reverse it.
Deliverable: One design at interface-and-schema depth with derived numbers and one written reversible tradeoff.
Day 4 — Only the fundamentals you will have to defend
- Write, in under two hundred words each, the answers to the two questions that follow almost any implementation: why this structure and not the obvious alternative, and what happens to this code at a hundred times the input.
- Write what an index actually costs: faster lookups on the indexed columns against a write that now maintains a second structure, plus the cases where the planner declines to use it anyway, low selectivity, or a predicate wrapping the column in a function.
- Delete any answer you cannot deliver aloud in under a minute, since an answer that needs reading is not an answer you have.
Deliverable: Three written answers, each under two hundred words and each timed aloud.
Day 5 — Your own work, timed
- Write a ninety-second and a four-minute version of your main project and time both aloud rather than reading them.
- Prepare the two follow-ups that always come: what you would do differently, and how you knew it worked.
- Put one number in the first sentence and be ready to say exactly where it came from and what it excludes.
Deliverable: Two timed narratives with one defensible number in the opening line.
Day 6 — The one full rehearsal, in the weekend block
- Run a sixty-minute mock covering a coding round and a design round in one sitting with no break, because sustained attention is the thing evenings have not trained.
- Immediately afterwards, and before hearing any feedback, write the three moments you lost the thread.
- Spend the rest of the block only on those three moments, and on nothing you merely feel shaky about.
Deliverable: Mock notes naming three failure moments with a specific fix written under each.
Day 7 — Taper
- Write the twenty-minute warm-up you will actually do on the morning: one problem you can already solve from a blank file, one design you can narrate, and nothing you have never seen.
- Re-read only your own notes from this week and open no new material.
- Write the logistics down: the editor or shared document you will be working in, whether execution and lookups are permitted, and the sentence you will use when you do not know something.
Deliverable: A one-page card holding the design structure, the project numbers, and the logistics.
The bug you decided not to fix
Engineers over-index on what they repaired. A stronger answer covers something you knowingly left broken: the alert you tuned down, the data inconsistency you documented instead of chasing, the cleanup you deferred past two quarters. Give the reasoning and the condition that would have reopened it, so it reads as a decision and not as neglect.
Reverse a fail-open decision on stale targeting snapshots
Hard · staleness · fail-closed · config propagation · reversal
Bidders hold an in-memory targeting index rebuilt and fanned out every few minutes. You originally decided that when fan-out stalls, a bidder keeps serving from its last snapshot, on the reasoning that stale targeting loses less money than not bidding. Then a creative was revoked on one exchange and kept serving for 40 minutes during a stalled fan-out. Describe a decision you reversed. Give the original reasoning on its own terms, the evidence that changed it, the new rule stated as a bound, and what the reversal cost.
Approach
- Defend the original decision honestly before overturning it. Fail-open was correct for the failure mode you had modelled, which was a fan-out stall during a period when nothing important had changed, and revenue loss from a fleet-wide no-bid is immediate and large.
- Name what the reasoning missed rather than calling it a mistake: snapshot age is a single signal covering two very different states, nothing changed and something contractual changed, and a bidder cannot tell them apart from inside its own process. You were trading against the average case while the tail case was a contract breach.
- State the new rule with a number and a location. Refuse candidates whose snapshot is older than a threshold derived from the tail of the observed fan-out interval, and drop them at the index rather than at the bid, so a paused line item, a lapsed approval and an absent approval row all disappear from candidacy by the same mechanism.
- Split the decision by consequence instead of applying one policy to everything: status and creative approval are contractual and fail closed, while a stale relevance model weight or a slightly stale frequency counter degrade quality and can continue. Note that a missing creative_approval row means not submitted, which is as ineligible as rejected.
- Solve the clock problem correctly, because it is where this design usually breaks: the bidder compares a monotonically increasing build sequence number issued by the index builder against what it holds, plus local receipt time, rather than comparing wall clocks across hosts.
- Give the cost you accepted and measured, and say how you communicated the reversal to the people who had argued for the original rule, including what you told them you had been wrong about.
Follow-up
- How do you choose the staleness threshold, and what data sets it?
- During a fan-out incident the whole fleet now fails closed at once. How do you keep that from becoming a larger outage than the one you were preventing?
- The revoked creative served for 40 minutes. What do you owe the exchange, and does the spend on those impressions stay in the ledger?
Argue against a synchronous budget reservation on the bid path
Hard · distributed systems · deadlines · availability · design review
A proposal reaches design review: make budget enforcement exact by having every bidder reserve budget from a central service before responding to a bid request. The finance stakeholder supports it because overshoot has been an audit finding twice. The exchange enforces an 80 millisecond deadline, the region runs on the order of 10^5 to 10^6 bid requests per second, and the bidders are stateless. Describe an argument like this that you made against a design. Give the reasoning, the number that carried it, what you proposed instead, and what you gave up.
Approach
- Concede first, on the record: the proposal really does deliver the tight bound, which is what was actually asked for. An argument that treats the other design as obviously wrong reads as reflexive and loses the room.
- Translate the cost into the currency of the deadline. A reservation adds one network round trip plus the reservation service's own tail latency inside an 80 millisecond budget that is already being decremented across hops. State what fraction of the budget that consumes at the 99th percentile, not the median, and say which existing step it displaces.
- Name the availability coupling explicitly: bidder availability becomes the product of its own and the reservation service's, so a dependency that was previously optional becomes mandatory on the highest-volume path in the system.
- Identify the real scaling limit, which is not aggregate throughput: reservations for one line item are a single hot key, so the binding constraint is contention on the most popular line items, and that load is heaviest exactly for the campaigns whose budget matters most.
- Point at the leak the proposal creates. Most bids lose, so most reservations are never spent and must expire, which reintroduces a timeout, a release path and a failure mode where a bidder dies holding reservations and the line item under-delivers until they lapse.
- Offer the alternative as a bound rather than a claim, with a number attached: locally applied probabilistic throttling, a shortened control period, and in-flight spend accounting, producing a stated overshoot that the finance stakeholder can accept or reject. Then say how the disagreement actually resolved and what evidence would have changed your mind.
Follow-up
- Suppose the cap is contractual rather than a target. What would you build, and would you still refuse a synchronous path for the last few percent of the budget?
- What happens to a line item when a bidder dies holding reservations, and how does the controller distinguish that from genuine under-delivery?
- How would you have proven your latency claim before the review rather than asserting it?
Estimate an attribution backfill you have never run
Medium · estimation · windowed state · backfill · uncertainty
A policy change extends the view-through window from 7 days to 28 days, and 90 days of attribution must be recomputed under the new window. You have never run this job, the identity keyspace has never been sized, and a long tail of batch-uploaded conversions is still arriving for periods inside that range. You have one day to produce an estimate the team will plan a quarter against. Describe estimating unfamiliar work: how you decomposed it, what you measured before committing to a number, and how you expressed the uncertainty that remained.
Approach
- Refuse the single number and hand back a decomposition with the unknown terms marked. Join state is roughly the identity keyspace times the window plus allowed lateness times per-key state size, and runtime is roughly events replayed divided by sustained per-worker throughput divided by worker count. Say out loud which of those four terms you do not know.
- Buy the unknown term instead of estimating it. Run one advertiser for one day, measure per-key state size and throughput, and note the identity_key null rate, which is a property of the source and not noise, since it directly reduces the joinable population.
- Give a range anchored to that measurement plus the specific fact that would move it, so the estimate is falsifiable. A range without a named sensitivity is the same guess wearing different clothes.
- Count the costs that actually dominate estimates of this shape and are not compute: restating periods already invoiced, downstream consumers that must re-read superseded revisions, and whether the job is resumable, since a non-resumable 90-day run has a failure cost equal to its whole runtime.
- Commit to a revision checkpoint rather than to the first number. Say when you will restate the estimate, what result would make you change the plan, and what result would make you stop and redesign.
Follow-up
- The pilot shows the 28-day state does not fit in memory. What changes, and what does each option cost?
- How do you tell a finance stakeholder that 90 days of already-reported numbers are about to move, and by roughly how much?
- What would you have done differently if you had been asked for the estimate with one hour instead of one day?