Model the data behind a professional network that is graded on hires, replies and confirmed mentorships instead of sessions and impressions. The engagement label arrives in milliseconds; the professional label arrives in ninety days, is never observed at all in sixty percent of cases, and half the time happens in a building you have no telemetry inside. One prompt, a fact table whose truth is not knowable for a quarter, a consent flag that must gate the write rather than the read, and a reputation score that has to be reproducible on demand or it is a rumour. A complete working-through of data flow, schema, consent-gated Python ingestion, long-horizon attribution SQL, the reproducibility check, and the dashboard that proves it.
Split the spine in two. fct_surfaced is written now and is immutable; fct_outcome arrives up to 120 days later and is joined through a re-runnable brg_outcome_attribution. Attribution is an opinion with a version, never a column on the fact.
attribution_run_id → 120-day outcome creditNOT EXISTS on absence → recruiter ghosting rateFiltering job-search signals at read time. Not looking has to gate the write — nothing is collected — or the toggle is a lie the schema can prove. And you cannot A/B a career: measure the challenger by shadow ranking, never by withholding a match.
Every recommendation system you have modelled before had a label you could read in seconds. A click, a like, a watch. This one does not. The thing this product exists to cause — someone gets a job — happens weeks after the row was written, in a building you have no telemetry inside, and more often than not you never find out it happened at all.
"Design the data platform for a professional network whose stated objective is professional outcomes rather than engagement — placements, replies that go somewhere, mentorships both sides confirm. A billion members, ten billion ranked items surfaced a day. Every surfaced item must be able to explain itself. A member can switch to 'not looking' and we promise we stop collecting job-search signal entirely. And the reputation score we show people has to be defensible line by line. How do you model it?"
Most candidates hear "professional network" and reach for the feed model they already know: impressions, engagements, a ranker, a rollup. That model is not wrong so much as it is answering a different question, and the interviewer is waiting to see how long it takes you to notice. A feed's objective function is available almost immediately — the like lands two seconds after the impression, in enormous volume, on the same platform, attributable to the row that caused it. Everything about the feed architecture follows from that gift. Take the gift away and the architecture inverts. Here the objective is a hire, and a hire is sparse (roughly one confirmed placement per four hundred million surfaced items), late (median forty-seven days from the surfaced row that started it, ninety-five percent inside a hundred and twenty-eight), and largely unobservable (about sixty percent of hires are never reported on the platform at all). You are being asked to build a measurement system for an event you mostly cannot see.
That single fact reorganizes everything. It means the fact table cannot carry its own outcome, because the outcome does not exist when the row is written and may never exist. It means attribution across a four-month window is a rebuildable opinion rather than a fact, so it needs a version and a re-run, exactly like ad attribution but with a hundred times the latency. It means engagement metrics are still collected but are explicitly not the objective, and the schema should make that demotion structural rather than cultural. And it means the counterfactual question — what would have happened if we had surfaced something else — cannot be answered the way a feed answers it, because you may not ethically withhold a good job match from a person to measure lift. So before any boxes and arrows, the working frame:
Scope first, because it is scored and most candidates skip it. Out of scope, stated explicitly: the ranking models themselves (treated as services that return scored items with reason codes attached), the messaging transport, the identity and authentication layer, the résumé parser, and the content moderation pipeline. In scope: how a surfaced item becomes an auditable row at ten billion a day, how an outcome that lands four months later is credited back without corrupting the immutable spine, how a member's declared intent gates collection rather than display, how a reputation score stays reproducible under scrutiny, and how a challenger ranker is evaluated without running an experiment on somebody's livelihood.
Then the envelope math, volunteered rather than extracted. LinkedIn-shaped numbers:
| Quantity | Estimate | Consequence |
|---|---|---|
| Members / monthly active | ≈ 1 B / 310 M | Persona dimension is large but slow-moving; the facts are what scale |
| Surfaced items / day | ≈ 10 B | ≈ 116 K/s average — the spine, and every row carries its reason codes |
| Interactions / day | ≈ 900 M | ~9% of surfaced items get a reaction; cheap, fast, explicitly not the objective |
| Job applications / day | ≈ 10 M | The last event before the platform loses sight of the process |
| Confirmed placements / month | ≈ 200 K | The actual objective — and the denominator problem below |
| Label density | ≈ 7 × 10⁻⁷ | One confirmed placement per ~1.4 M surfaced items. The row that shapes the architecture |
| Surfaced → placement lag | 47 d median | p95 is 128 days — hence a 120-day window, and an interval join rather than an equality |
| Outcomes never observed | ≈ 60% | Most hires happen off-platform; the label is missing-not-at-random |
| Intent-mode changes / day | ≈ 4 M | SCD2 on the persona, and it gates ingestion at event time |
| Hottest requisition | ≈ 40 K / 48 h | A viral posting takes 40 K applications in two days; partition by applicant, never by requisition |
Notice which row does the architectural work. The ten-billion firehose sets the bill and the partitioning, and the viral requisition sets the write key — both are familiar problems and neither is what this question is about. The row that dictates the shape of the design is the label density: seven in ten million. At that sparsity, a model trained on the objective directly would see almost no positive examples, and a dashboard reporting the objective daily would be reporting noise. Everything that follows — the interaction grain kept as a leading indicator, the attribution bridge that can be rebuilt when the definition improves, the shadow-ranking table, the honest observability tile that reports how much of the label you are missing — exists because the thing you are optimizing for is nearly invisible.
This scenario is the data-platform working-through of a product argument made at length in “The Feed Is Winning. Your Career Isn’t.” — which asks what a professional network optimized for outcomes rather than attention would actually look like on screen.
There is a working desktop prototype of that product at paddyspeaks.com/careeros. The reason codes, the eight-dimension reputation panel and the “not looking means not collected” rail in this schema are all visible there as interface.
The serving path writes a surfaced row in milliseconds with its explanation already attached. The outcome path collects sparse, late, half-external truth for the next four months and stitches it back through a bridge that can be rebuilt from scratch. Between them sits the consent gate, which is the only component in the picture that is allowed to refuse to write.
Four properties of this picture do most of the interview's work. First, the consent gate sits before the log, not after it. When a member's intent mode is “not looking”, job-search signal is never written — not written-then-hidden, not written-then-filtered. That placement is the difference between a promise and a checkbox, and it is checkable: an anti-join over the spine should return zero rows for those persona-days, and that query belongs in the test suite. Second, the reason codes are produced by the ranker and written onto the surfaced row at serve time. An explanation reconstructed later from a re-scored model is not an explanation of what happened; it is a plausible story about a different ranking. Third, the outcome never edits the spine. A placement discovered in November does not go back and update an August row — it lands in its own fact and is connected through a bridge, so the August row still says exactly what the system believed in August. Fourth, the shadow table records what the challenger model would have surfaced, alongside what the champion actually did, suppressing nothing.
Write what you knew, when you knew it; discover the consequence later; never let the discovery rewrite the knowledge. The spine is immutable because it is a record of a decision, and decisions do not improve retroactively. The outcome is append-only because a hire that falls through in week two is a second event, not a correction of the first. And the attribution between them is versioned because the definition of “this recommendation led to that job” is genuinely contested — marketing will want first-touch, the ranking team will want last-touch, and the honest answer is that you store the surfaced rows and the outcomes and let the run id decide, so the argument is settled by re-running a job rather than by rebuilding a warehouse.
Four facts and three dimensions. The surfaced row carries its own explanation. The outcome row carries its own provenance. The bridge between them carries a run id. And the persona dimension is SCD2 not because titles change — though they do — but because the consent scope changes, and every row in the system has to be answerable against the scope that was in force when it was written.
This is the most-written table in the design and the one that makes the product's central promise keepable. Alongside the usual serving context — model id, predicted score, slot — it carries two arrays that most recommendation schemas do not have: reason_codes, the ranked inputs that would be shown under “Why this?”, and not_used_codes, the inputs the product publicly commits to not ranking on. Storing the negative space sounds like decoration until an auditor asks you to prove that a role recommendation did not use an inferred demographic, and you can answer with a column instead of a deposition.
The reason_present constraint is worth defending out loud, because it is the schema enforcing a product promise. If the ranker cannot say why it surfaced something, the row does not get written and the item does not get shown. That is a strong commitment and an interviewer will push on it — what about a cold-start recommendation with no meaningful signal? The answer is that “insufficient evidence to explain” is itself a reason code with a low confidence, and an item carrying it is shown in a labelled exploration slot rather than smuggled into the main ranking pretending to be a match.
The outcome table is small, slow, and disproportionately important. Its distinguishing column is observed_via: platform-observed events are cheap and trustworthy but cover only the early funnel; self-reported outcomes cover the part that matters most and are unverifiable; employer-verified outcomes are rare and authoritative. Collapsing those three into one boolean would be the single most damaging simplification available here, because every downstream metric needs to state which kind of evidence it rests on.
Two timestamps, and candidates who only write one lose a point. outcome_ts is when the thing happened; recorded_ts is when the platform learned of it. The gap between them is often weeks, and it is the reason every outcome-based metric must be reported as a cohort that is still filling in. A placement rate for last month is not a number, it is a number and a maturity — and a dashboard that shows the first without the second will show a cliff every time it refreshes.
This is the table that makes the design senior. A hire is caused by a chain of surfaced items over four months: the role that appeared in a briefing in June, the hiring manager who surfaced in July, the article that got read before the interview. Which of those gets the credit is a modelling choice, and modelling choices change. So credit does not live on either fact. It lives in a bridge keyed by an attribution_run_id, where a re-run under a new definition produces new rows and leaves the old ones intact — and the two can be compared.
A persona is (member × intent mode), and it is SCD2. Alex Chen job-hunting in March and Alex Chen not-looking in September are two persona rows for one member, with different consent scopes, different collected signals, and metrics that must never be summed across the boundary. This is the dimension that gates ingestion, so its valid_from / valid_to are not a reporting convenience — they are the authority a writer consults before it is allowed to persist anything.
The objective column on the model dimension is a one-word governance mechanism, and it is worth pointing at explicitly in an interview. The product's whole claim is that it ranks on professional outcomes rather than attention. A model registry that records which objective each deployed ranker was trained against turns that claim into something you can query: show me the share of surfaced rows in the last quarter served by a model whose objective was engagement. If that number drifts upward over eighteen months, the thesis is eroding, and the schema noticed before the strategy deck did.
Reputation is computed, never stored as an input. The ledger holds claims and the confirmations attached to them; the daily mart holds the computed dimension scores together with the scoring_model_id and a hash of the evidence set they were computed from, so any score can be recomputed and diffed against what was displayed. A number a member cannot interrogate is a rumour with a font.
One spine, two opposite disciplines. Everything about what was collected is decided once, at write time, and frozen forever. Everything about what it meant is decided at read time, versioned, and allowed to change. Candidates who put both at the same end of the pipeline get a system that either lies about privacy or cannot improve its own definitions.
Take the privacy half first, because it is the one the product markets. A member switches to not looking. The tempting implementation — the one nearly every real platform ships — writes the same events it always did and adds a flag to the read path so job-search signal is hidden from recruiters. It is cheaper, it is reversible, and it means the toggle is a lie: the profile-view scoring, the role-affinity model and the availability inference all keep running, and the member's own employer is one internal query away from what they were promised nobody could see. The honest implementation refuses at the gate. If job_search is not in the persona's collects array at event time, the writer drops the event and increments a counter. Nothing is stored; there is nothing to leak, nothing to subpoena, and nothing to quietly re-enable.
The consequence is one that a good interviewer will immediately probe, so volunteer it: the data is gone, and it does not come back. When the member switches back to job hunting in September, there is no six-month history to warm-start their recommendations, because the six months were never recorded. You take a genuine cold-start penalty in exchange for the promise being true. That trade is the answer — and being able to name its cost is what distinguishes someone who has thought about it from someone reciting a privacy slogan.
Now the attribution half, which pulls in the opposite direction. What caused a hire is not knowable at write time and is not stable afterwards. First-touch flatters discovery surfaces; last-touch flatters the jobs page; a position-decay model over a hundred and twenty days is defensible and will still be argued about. Because the argument never ends, the design refuses to commit: the spine records what was surfaced and the outcome fact records what happened, and the credit connecting them lives in a bridge stamped with a run id. Improving the model is a job that writes new rows. Last quarter's board numbers remain reproducible because the run that produced them still exists.
The two disciplines are complementary, and stating the pairing out loud is the strongest single sentence available in this interview: collection is decided once and never revisited; interpretation is revisited forever and never destructive. A system that gets these the wrong way round — flexible collection, frozen interpretation — is precisely the system that quietly harvests everything and can never explain any of it.
A feed measures a challenger ranker by withholding the champion's output from a holdout slice. You cannot do that here. Suppressing a strong job match from a real person for four months to measure lift is an experiment on someone's livelihood, and “we had a holdout” is not a defence anybody wants to read in a deposition. The design answer is shadow ranking: run the challenger over the same request, log what it would have surfaced into fct_shadow_surfaced, and show the member the champion's output anyway. You lose clean randomization and you gain the ability to sleep — and you recover most of the statistical power through the overlap set, comparing outcomes on items both models chose against the ones only one of them did. Say the limitation out loud: shadow evaluation is biased by the champion's own exposure, and the honest correction is a small labelled exploration slot, disclosed to the member as exploration, rather than a silent suppression.
Three programs carry the system. The consent-gated writer that refuses to persist what it was not permitted to collect, the outcome stitcher that reconciles three disagreeing sources without letting any of them overwrite another, and the attribution runner that spreads credit backwards across a hundred and twenty days and stamps a version on its own opinion.
This sits on the serving hot path, ahead of the log. It resolves the persona's consent scope from a cache of the SCD2 dimension, drops any event whose signal family is not in collects, and — the part that turns a policy into an operable system — records that it dropped one. A suppression counter is what lets the dashboard show that the gate is alive; a gate nobody measures is indistinguishable from a gate that was quietly removed in a refactor eighteen months ago.
One carve-out, always stated: the gate resolves consent as of the event timestamp, not as of now. Events arrive out of order and sometimes hours late, and a member who switched to not looking at noon must have their 11:55 events written and their 12:05 events dropped. Using the current persona would either retroactively suppress data that was legitimately collected or, far worse, admit data collected after the promise took effect.
Platform events, self-reports and employer verifications all describe the same hire and none of them agrees on the date. The naive implementation picks a precedence order and overwrites; the correct one keeps all three as separate rows carrying their provenance and marks a canonical view, so a metric can always state which evidence it is standing on and an analyst can always ask how the answer changes if self-reports are excluded.
This is the batch job that connects the two clocks. For each outcome it looks back across the attribution window at everything the product surfaced to that member about that item, and spreads credit with a decay that respects both recency and position. It writes into the bridge under a fresh run id and never touches an existing one. The interesting detail is what it does when it finds nothing: an unattributed outcome is recorded as such rather than silently dropped, because the share of outcomes the product cannot claim credit for is one of the most honest numbers in the entire system.
Every aggregate in this system has a problem no feed metric has: it is incomplete in a way that changes for months after the period closes. The aggregation layer's real job is not speed, it is honesty — reporting outcome rates as maturing cohorts, recomputing reputation from the evidence ledger rather than incrementing it, and refusing to publish a rate whose denominator can be gamed.
Start with the maturity problem, because it is the one that embarrasses teams in public. Placement rate for June, computed on the first of July, will be roughly a third of placement rate for June computed on the first of November — not because anything improved, but because the outcomes had not arrived yet. A dashboard that reports the first number will show a catastrophic drop every month and a miraculous recovery every quarter, and the ranking team will chase both. The fix is structural: every outcome-based aggregate is stored per cohort and per observation age, so the number June had at thirty days can be compared against the number May had at thirty days. You never compare a fresh cohort to a mature one, and the mart makes that comparison the easy one to write.
Then the denominator problem, which is where the trust metrics live and where an interviewer can profitably push. The product promises to show candidates a recruiter's response rate before they reply — the recruiter's own scorecard, visible to the person deciding whether to invest hope in a conversation. That number is only meaningful if the denominator cannot be curated. If “messages received” is what the recruiter's tooling chooses to mark as received, the rate is theatre. So the denominator is defined on the platform side of the boundary — first contacts that were delivered — and the numerator counts any terminal state including rejection, because a fast no is a good outcome and a system that penalizes it teaches recruiters to ghost.
Reputation follows the same discipline in a different key. The score is recomputed from the evidence ledger every day rather than incremented, because an incremented score drifts away from its evidence and can never be reconciled back. Recomputation is affordable precisely because the evidence grain is small — claims and confirmations number in the millions per day, not billions — and it buys the property the product is selling: any member can ask which rows produced their number, and the answer is a join rather than an apology. Low-sample dimensions are marked is_provisional rather than scored confidently, and dimensions that do not apply to someone's work are marked is_applicable = false rather than scored zero, because a zero reads as a failing grade and a blank reads as the truth.
Four queries, each carrying a pattern worth having. The interval join that credits a hire back across four months, the anti-join on an absent event that measures ghosting, the as-of replay that proves a reputation score reproducible, and the assertion query whose entire purpose is to return nothing.
The query the whole schema exists to make possible. Because ranking_model_id rides every surfaced row and credit lives in a versioned bridge, comparing an outcome-trained ranker against an engagement-trained one is a GROUP BY — not a bespoke experiment pipeline. The senior framing is to report both the outcome rate and the interaction rate, because the interesting and uncomfortable result is a model that wins on clicks and loses on hires, which is exactly the thesis under test.
The trust metric candidates can see before they reply. Its craft is entirely in the definition: the denominator is delivered first contacts, which the recruiter does not control, and a rejection counts as a response, because a system that treats a fast no as a failure will manufacture silence. Ghosting is the absence of any terminal state within fourteen days — an absence, which means the pattern is a NOT EXISTS. Nothing happened, and the nothing is the measurement.
The query that turns “every score is traceable” from a slogan into a test. Recompute a dimension from the evidence ledger as it stood on a past date — using only confirmations that existed then, under the scoring model that was live then — and diff it against what was actually displayed. Any row that comes back is either a bug in the scoring job or a silent backfill someone did not disclose, and both are things you want to find before a member does.
The shortest query on the page and the one that decides whether the product's central promise is real. For every persona-day where the consent scope excluded job-search collection, there must be no job-search row on the spine. It is an anti-join, it belongs in the nightly test suite as an assertion rather than a report, and its correct output is nothing at all.
A senior design ends with observability, and here observability has an unusual job: it has to report how much of the truth is missing. Four panels — are outcomes actually happening, is the ranker earning them rather than earning clicks, is the trust layer holding, and is the consent gate still alive.
Read the panels against each other and the dashboard argues the whole thesis on its own. The divergence chart is the centrepiece: the two engagement-trained models on the left top the interaction rate and sit near the bottom on placements, while the outcome-trained models invert exactly that ordering. That is the finding the entire schema was built to be able to produce, and it is only producible because objective is a column on the model dimension and credit is a versioned bridge rather than a hardcoded join. Meanwhile the engagement-objective share sits at eighteen percent and trending up — some surfaces have quietly reverted to a click-trained ranker, which is exactly the eighteen-month erosion the product's own strategy document warns about, caught by a tile rather than a retrospective.
And the two flat zeros at the bottom right are the most important numbers on the board precisely because they are boring. Zero leak-audit rows means the consent gate held every day this quarter. Zero replay drift means every reputation score displayed today can be recomputed from its evidence and lands on the same number. Neither tile will ever be interesting, and both should page someone the instant they stop being zero. Next to them sits the suppressed-events counter at forty-one thousand a minute — the gate visibly doing its job. A team that only alarms on the leak count would not notice the far more likely failure: a refactor that removes the gate entirely, at which point the leak count stays at zero because nothing is being refused, and the suppression counter silently falls off a cliff.
Strip the professional-network details away and the question was testing six judgments, every one of which generalizes to any system whose objective function is slow, sparse and partly unobservable — clinical outcomes, education, credit, insurance, anything where the thing you care about arrives long after the thing you did.