A Software Engineer at SpaceXAI operates at the convergence of frontier artificial intelligence, massive-scale distributed infrastructure, and high-impact consumer platforms. Following the integration of xAI and SpaceX engineering organizations, this role powers systems that serve over 600 million global users on the X Platform, runs real-time inference and training on elite compute clusters, and builds mission-critical backend systems across products like X Core Product, X Money, Ads Product, and Search Infrastructure.
In this position, you are not merely maintaining existing services; you are independently owning core components end-to-end. Whether you are scaling low-latency microservices, implementing high-throughput streaming pipelines, or building AI-native developer tooling, your work directly affects core business metrics and user experience. The organization operates with a flat structure that values speed, engineering rigor, and direct contribution over hierarchical management.
The engineering environment at SpaceXAI demands a extreme bias toward action, absolute technical ownership, and rapid adaptability. Engineers work directly with systems written in Python, Rust, Go, and Java, tackling complex problems in high-concurrency datastores, distributed caching, custom AI integration, and live debugging. Joining as a Software Engineer means pushing production code in an environment where individual initiative defines leadership.
Initial Screening
reportedBefore 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
Technical Assessment
reportedThe same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
System Design Scenarios
reportedYou cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
Project Ownership Evaluation
reportedMost of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Final Assessment
reportedA day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.
What to demonstrate
- Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
- Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
- Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
- Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing
How to prepare
- Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
- Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
- Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one
PracHub editorial advice for the preparation topics above.
Checking a quota with a select and then writing
Under read-committed isolation, two concurrent transactions both observe a count below the limit and both insert, so the limit is exceeded by exactly the concurrency. Repeatable read does not rescue it either: it provides a stable snapshot, and this is write skew, which snapshot isolation permits by design. The options are serialisable isolation, which detects the conflict and aborts one transaction with a serialisation failure and therefore obliges the caller to retry; a single statement with the predicate inside the write; or a constraint that makes the surplus insert fail outright. The reason this pattern survives review is that it is correct in every test that runs one request at a time.
Serialising a tenant's writes through select ... for update on a single counter row
It is the first change that makes a counter correct, and it caps that tenant's write throughput at roughly one divided by the lock hold time. A transaction that takes the lock, makes a network call and then commits holds it for the entire round trip: at 2 ms that is about 500 writes per second for the whole tenant, and the largest tenants are exactly the ones that exceed it. The damage then spreads, because every waiter holds a database connection while it queues, so one hot tenant drains the shared pool and the symptom presents as a site-wide latency incident rather than as a lock problem. The repairs are to shrink the critical section to a single statement, to shard the counter into per-(tenant, hour) or per-(tenant, bucket) rows and sum on read, or to batch in memory and flush periodically while accepting the bounded loss that batching implies.
A cache with no invalidation story
Say how an entry goes stale, how long you can serve it stale, and what happens when many requests miss the same key at the same instant. One popular key expiring under load sends every concurrent request to the origin together; single-flight coalescing, jittered expiry, or serving stale while revalidating are the standard answers.
Arguing past a hint
When the interviewer asks what happens for a particular input or floats a different data structure, stop and take it seriously; it is almost always a correction rather than idle curiosity. Talking over it converts a recoverable wrong turn into a data point about how you handle review.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Find peak concurrent sandbox usage from run intervals
Given up to 5 million job_run rows for one tenant over one day, with run_id, started_at, finished_at, status and wall_clock_limit_seconds, report the maximum number of sandboxes running at once, the earliest instant that maximum is reached, and the first run_id that would breach a per-tenant cap of C. started_at is null while a run is queued; finished_at is null both for runs still executing and for runs in status lost. Treat a run as occupying [started_at, finished_at). Give the complexity and state how you handle each null.
Approach
- Turn each run into two sweep events,
(started_at, +1)and(end, -1), then sort the 2n events by timestamp with-1ordered before+1at equal timestamps. That tie-break is what makes the interval half-open, so a run finishing at 10:00:00 and one starting at 10:00:00 never overlap. - Decide each null out loud before sweeping, because each choice moves the answer. A null
started_atmeans queued and contributes nothing. A nullfinished_atwith statusrunningorleasedis clipped to the window end. Statuslosthas no observed end at all, so clip it atstarted_at + wall_clock_limit_secondson the grounds that the supervisor owns the timeout, and record that you did. The table'scheck (finished_at is null or started_at is not null)guarantees you never see an end without a start. - Sweep once, maintaining a running counter, the maximum, and the timestamp at which the maximum was first attained (update
peak_atonly on a strict increase, or you will report the last such instant instead of the earliest). Capture the firstrun_idwhose+1takes the counter to C+1 during the same sweep rather than in a second pass. - Complexity: O(n log n) dominated by the sort, O(n) space. If rows already arrive ordered by
started_at, a min-heap of end times gives O(n log k) time and O(k) space with k the peak concurrency, which is the better shape when the rows come from an index scan on(tenant_id, started_at). - If second resolution is acceptable, counting-sort the endpoints into an 86,400-slot delta array and prefix-sum it: O(n + T) time and O(T) space, which beats the comparison sort at 5 million rows. It answers only at second granularity, so state which resolution the cap is defined in.
Follow-up
- Now report peak concurrency per tenant for 10,000 tenants from one globally sorted stream. What changes about memory and about the sort?
- The cap has to be enforced at dispatch rather than reported afterwards. What does the admission check look like, and where does it race?
- How would you answer 'peak concurrency within any 5-minute window' without re-sorting?
Schedule ordered webhook retries with a heap of subscription queues
Design the in-memory scheduler for webhook delivery. Up to 20 million rows sit in status pending or failed_retryable across 200,000 subscriptions, each row carrying next_attempt_at and attempt_count, and each endpoint having a circuit breaker. Deliveries for one subscription must be attempted in order, so at most one attempt per subscription may be in flight. Support due(now), complete(delivery, outcome) and insert(delivery) in O(log S), where S is the subscription count rather than the delivery count. Give the backoff formula you schedule retries with.
Approach
- Key the global heap by subscription, not by delivery. Each subscription owns a FIFO of its due deliveries in event order; the heap holds one entry per eligible subscription, keyed by its head's
next_attempt_at. That is 200,000 heap entries instead of 20 million, and it makes the one-in-flight rule structural rather than a check somebody can forget. due(now): peek the minimum. If its key is in the future, sleep until then instead of spinning. Otherwise pop it, move the subscription into an in-flight set, and do not re-push it. A subscription absent from the heap cannot be dispatched twice, which is precisely how ordering is preserved.complete: on success, drop the head and re-push the subscription keyed by its new head, or leave it out when the queue empties. On a retryable failure, incrementattempt_countand setnext_attempt_at = now + uniform(0, min(cap, base * 2^attempt)), sampled uniformly across the whole interval. That is full jitter; deterministic backoff re-synchronises the herd you just created.- Circuit breaker: park the subscription in a second heap keyed by its half-open time, so an endpoint dead for six hours costs one heap entry and zero attempts rather than consuming worker slots. Admit exactly one probe at half-open and close the breaker only on its success.
- Say the price of the ordering guarantee out loud. One in-flight attempt per subscription means an endpoint answering in 10 seconds drains at 0.1 deliveries/second however many workers you run, and its backlog grows until it recovers. If the customer does not need order, allow k in flight and document delivery as unordered; that is the trade, and it is a product decision.
- All three operations are O(log S) with O(S) resident heap memory and the queues themselves backed by the store. The database-backed equivalent is a partial index on
(subscription_id, next_attempt_at) where status in ('pending','failed_retryable')claimed withFOR UPDATE SKIP LOCKED, and the write-back must be fenced onlease_tokenso a worker that stalled and resumed cannot overwrite a newer attempt.
Worked solution 30 min
- Define the four structures explicitly:
queues: subscription_id -> deque[delivery],ready: min-heap of (next_attempt_at, subscription_id),inflight: set[subscription_id],breaker: min-heap of (half_open_at, subscription_id). - Write down the invariant you will assert after every operation: a subscription appears in at most one of
ready,inflightandbreaker, never in two. - Implement
due,completeandinsert, then simulate 200,000 subscriptions with Zipf-distributed queue depths totalling 20 million deliveries. - Add one endpoint that always times out after 10 seconds and one that always answers in 20 ms, then measure the fast endpoint's throughput with and without the per-endpoint breaker.
- Instrument heap size across the run.
Follow-up
- One subscription has 4 million queued deliveries. What stops it from starving the other 199,999, and what does your heap look like under that load?
- A customer requests redelivery of last Tuesday's events. Where do those rows enter your structure, and what keeps them from reordering live traffic?
- The process restarts. How much state do you rebuild, and what stops every subscription from being attempted in the same second?
Fold a deduplicated usage stream into hourly rollups
You are given one day of usage_event rows, up to 250 million, each carrying event_id, tenant_id, workspace_id, environment, sku, quantity numeric(20,6), idempotency_key, occurred_at and ingested_at. Produce usage_rollup_hourly cells keyed (tenant_id, workspace_id, sku, hour_start) with quantity_sum, event_count and source_max_ingested_at. An event counts once per (tenant_id, idempotency_key). The rollup grain has no environment column, so state your filter. One pass. Give your time and space bounds, and say what the deduplication actually costs in memory.
Approach
- Bucket on
occurred_at, neveringested_at:hour_start = date_trunc('hour', occurred_at at time zone 'UTC'). The two columns answer different questions.occurred_atsays which hour the customer is billed for;ingested_atsays how current the fold is. Using the second for the first makes late data invisible instead of correctable. - The fold is trivial and the deduplication is the entire cost, so price it before designing anything clever. An exact set over
(tenant_id, idempotency_key)at 250M entries, stored as a 16-byte 128-bit hash in an open-addressed table at 0.7 load factor, needs about 357M slots at 16 bytes each, roughly 5.7 GB. The fix is partitioning byhash(tenant_id) % Pso each shard holds 1/P of the set and no tenant's keys straddle shards. - Rule out a Bloom filter as a replacement, in the right direction: a false positive reports 'already seen' for an event never seen, so you drop a real event and lose revenue with no error raised. It is usable only as a negative pre-filter in front of the exact set, where a miss is conclusive and a hit must fall through to the real lookup.
- Accumulate in scaled integers, not binary floating point.
numeric(20,6)admits values below 10^14, so one event scaled to micro-units can reach 10^20, past int64's 9.22 x 10^18; use a 128-bit or arbitrary-precision accumulator unless you first bound the per-event maximum. binary64 represents integers exactly only to 2^53, about 9.01 x 10^15, and cannot represent 0.1 at all, so two runs that sum in different orders disagree. - Carry
source_max_ingested_at = max(ingested_at)over the events folded into each cell, and countevent_countover accepted, post-dedup events. Without that watermark there is no way to prove later what a number did and did not include, which is the first question any reconciliation asks. - State the environment filter explicitly, because the rollup grain cannot record it. A fold that quietly includes
stagingbills non-production traffic; one that quietly excludes it loses a cost signal. Production-only is the billing answer, and either way it belongs in the job name and the output metadata. Complexity: O(n) time, O(distinct dedup keys) space, dominated by the dedup set rather than by the cells.
Follow-up
- A producer retries at 23:59:59 and the retry lands at 00:00:01. The unique index on the daily-partitioned table must include the partition key. What gets double-counted, and what is the smallest change that fixes it?
- The consumer acknowledges its batch before committing the fold. Which failure loses revenue now, and which arrangement duplicates instead?
- What makes a re-run over the same day produce byte-identical rollups?
Model credential revocation so history survives the delete
tenant_api_key stores key_id, tenant_id, workspace_id, name, key_prefix, secret_hash, scopes text[], status (active, revoked, expired, compromised), auth_version, created_at, expires_at, last_used_at, revoked_at, revoked_reason. Rotation inserts a new row and revocation never deletes, because an incident review asks which credential served a request last quarter. Write the constraints that enforce: a label is unique only among a tenant's live keys, revoked_at and status can never disagree, and scopes is never empty. Then write the authentication lookup predicate, and name one column in this table that must stay out of it.
Approach
- Reach for a partial unique index rather than a plain UNIQUE:
create unique index on tenant_api_key (tenant_id, name) where revoked_at is null. Any number of revoked rows may share a label, the live namespace stays unique per tenant, and the revoked majority is not in the index at all, so it stays small on a table that only grows. - Tie the nullable timestamp to the enum so the two cannot drift:
check ((revoked_at is not null) = (status in ('revoked','compromised')))andcheck ((revoked_at is null) = (revoked_reason is null)). A revocation that records no reason is the one an incident review cannot use. - Write the emptiness check as
check (cardinality(scopes) > 0), notarray_length(scopes, 1) > 0. array_length returns NULL for an empty array, a CHECK constraint passes when its expression is NULL, so the array_length version accepts exactly the value it was written to reject. - Make the lookup a single index probe with every liveness condition inside it:
where secret_hash = $1 and revoked_at is null and (expires_at is null or expires_at > now()) and auth_version = $2, backed by a unique index on secret_hash. Nothing is filtered in application code, so there is no path that forgets a clause. - Keep last_used_at out of that predicate. It is written asynchronously and is allowed to lag by a minute, so it is a usage signal; feeding it into an authorisation decision makes the decision depend on a write that may be late, batched away or lost.
- Flag the modelling smell while you are here:
expiredis derivable fromexpires_at < now(), so storing it as a status obliges a job to keep it true and guarantees the column is wrong between the expiry instant and that job's next run. Derive it in the predicate; keep the stored status for states that are decisions rather than clock readings.
Follow-up
- Rotation issues a replacement while the old key stays live for a 30-day overlap. What does the uniqueness rule become, and what does the UI show to tell two same-named keys apart?
- A password reset bumps the principal's auth_version. No row in this table changed. How does the next request fail, and what query counts how many keys that bump just killed?
- A key turns up in a public repository. Which columns let you find it, and what do you write to the row?
Migrate a live partitioned event table without blocking ingest
usage_event is range-partitioned daily on ingested_at, holds roughly 250M rows per day across 400 live partitions, and is written at 10-40k rows/second. Two changes are required: quantity must move from double precision to numeric(20,6), and a new environment column must become NOT NULL with a default of 'production'. Ingest cannot stop. Give the ordered plan, naming for each step the lock it takes, what that lock blocks, and roughly how long it is held. Identify the one step that cannot be rolled back cleanly once traffic depends on it.
Approach
- Classify the two changes before planning anything. Adding a column with a non-volatile default has been metadata-only since PostgreSQL 11, so it is cheap. Changing double precision to numeric is not binary-coercible, so
alter column ... typerewrites every partition under ACCESS EXCLUSIVE and rebuilds its indexes; on this volume that is hours of blocked ingest and is simply not an option, which is why the plan is expand-and-contract rather than one statement. - Expand: add
quantity_numeric numeric(20,6)andenvironmentwith its default on the parent. Both are catalogue-only but both take a brief ACCESS EXCLUSIVE that cascades to partitions, so run each withlock_timeoutset to a second or two and retry on failure. A queued ACCESS EXCLUSIVE request blocks every reader behind it, which is how a metadata-only change turns into an outage. - Dual-write: deploy producer code that populates both columns on every insert, and leave it running before anything reads the new column. This is the step that cannot be reverted cleanly. Once readers depend on quantity_numeric, reverting the writer leaves rows with a null there, and the gap is only discoverable by re-reading the old column, which the readers have stopped doing.
- Backfill older partitions in batches keyed on the primary key, oldest first, committing every few thousand rows with a pause between batches, and skipping the partition still receiving writes until it rotates. Each batch is an ordinary UPDATE taking row locks only. The cost is bloat and WAL rather than blocking, so watch dead tuples and let autovacuum keep pace instead of wrapping 400 partitions in one transaction.
- Make NOT NULL cheap with the three-step form:
add constraint ... check (environment is not null) not valid(brief ACCESS EXCLUSIVE, no scan), thenvalidate constraint(SHARE UPDATE EXCLUSIVE, scans while reads and writes continue), thenset not null, which from PostgreSQL 12 uses the validated check and skips its own full scan. Do this per partition, then on the parent. - Switch and contract: move reads to the new column behind a flag, verify over a full period that both columns agree on freshly written rows, drop the old column (metadata-only), and only then remove the dual-write. Any index on the new column goes on with CREATE INDEX CONCURRENTLY per partition, since CIC is not supported on a partitioned parent: create the parent index with ONLY, build each child concurrently, then ALTER INDEX ... ATTACH PARTITION until the parent index becomes valid.
Worked solution 45 min
- On a scratch cluster, build 10 partitions of 2M rows each and run a writer at a few thousand inserts/second.
- Run the naive type change and measure how long writes stall and how far ingest lag grows before killing it.
- Run the expand step with
lock_timeout = '2s'while the writer runs, and observe a clean lock timeout and retry instead of a pile-up of blocked readers. - Backfill in 5k-row batches and chart dead tuples and WAL generated per batch.
- Run the not-valid, validate, set-not-null sequence and confirm from
pg_stat_activityand timings that nothing held an exclusive lock through a full scan. - Add an index with CIC per partition plus ATTACH PARTITION and confirm the parent index reports valid only after the last attach.
Follow-up
- A CREATE INDEX CONCURRENTLY fails halfway through the partition list. What state is the table in, how do you detect it, and what do you run?
- The producer computes quantity itself. What happens to a request already in flight when the dual-write deploy lands, and does it matter?
- Give two queries that prove the backfill is complete: one cheap enough to run every minute, one authoritative.
Design the batch ingest endpoint metering agents retry into
A customer-run agent posts usage events in batches of up to 1,000 to metering-ingest with a 30-second timeout and at-least-once retry of the whole batch. Each event carries event_id, idempotency_key, sku, quantity and occurred_at; the server adds ingested_at, and usage_event is partitioned daily on ingested_at with unique (ingested_day, tenant_id, idempotency_key). Design the endpoint: the request shape, what the response says when 900 events are new, 90 are duplicates and 10 are malformed, the status code, and the agent's algorithm on timeout. Then state the deduplication horizon and justify it against that unique constraint.
Approach
- Fix the per-item outcome taxonomy first, because the status code follows from it: accepted, duplicate, and rejected with a permanent code. A duplicate is a success; reporting it as an error makes the agent either re-send revenue it already delivered or drop it.
- Allow only permanent failures per item. A transient per-item failure inside a 200 invites the agent to discard that event, so anything transient escalates to a 5xx for the entire batch. A 200 is then a promise that every event not marked rejected is committed and durable.
- Return 200 with a results array aligned by index and carrying the event id, so the agent can retry precisely the subset that needs it and quarantine the ten malformed events instead of hard-looping a poison batch forever. Cap the batch at 1,000 items and a byte size, with 413 beyond it and 429 with Retry-After for backpressure.
- Deduplicate per event, never per batch: the agent may split, merge or reorder a retried batch, so a batch-level key matches nothing on the second attempt. The key is (tenant_id, idempotency_key), and the tenant comes from the resolved credential; a tenant id present in the body is compared against it, never trusted.
- Size the horizon as a correctness parameter. The unique index includes the partition key, so it deduplicates only within one day: a retry that crosses midnight, or a replay run a week later, passes straight through it. A separate dedup store keyed (tenant_id, idempotency_key) with a TTL exceeding the agent's maximum retry window plus the longest replay you intend to support is what actually enforces the invariant, which makes its retention a correctness setting rather than a cost knob.
- Order the commit against the response and the acknowledgement: commit then respond at the endpoint, and downstream commit the fold then acknowledge the message. Acknowledging first turns a crash into silently lost revenue with no error raised anywhere.
Follow-up
- The agent times out at 30 seconds having received nothing. What exactly does it do next, and what in your design makes that safe?
- Ten events are rejected every hour for a week and nobody notices. What does the endpoint owe the customer beyond a per-item 4xx code?
- A replay pushes 40 million events through this endpoint in an hour. Which part of your design degrades first?
Publish limit semantics that clients can back off against
The gateway enforces three separate limits: a per-tenant token bucket, a monthly plan quota, and a cap on concurrent in-flight requests. Tenants hold several credentials and call from three regions, and today they see undifferentiated 429s. Design the client-facing contract: which headers carry which limit, the status codes that distinguish slow down from out of plan allowance from too many in flight, and what a well-behaved SDK does for each. Also state what the gateway returns when the shared counter store is unreachable, and what bound that choice implies.
Approach
- Separate the three objects before naming a header. A bucket refills continuously, a quota does not refill until the period rolls, and a concurrency cap clears when an in-flight request finishes. They have different remedies, so they cannot share a status code.
- Map them accordingly: 429 with Retry-After for the bucket, where waiting works; 429 with a distinct code and a reset hint for concurrency, where waiting works only if something else completes; and 402 or 403 with a plan code for quota, where waiting never works and the SDK must surface it rather than sleep on it.
- Emit RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset for the bucket only, and document Remaining as advisory. It comes from a shared counter that other requests are changing concurrently, so a client treating it as a reservation has built a race.
- State the aggregation in the docs: the limit applies per tenant across every credential and every region. The cheap per-pod bucket of rate/N is correct only when traffic spreads evenly, and a tenant whose connections land on a few pods is throttled well under its published limit while a widely spread one exceeds it.
- Decide the degraded mode in advance and publish it. Failing open serves unmetered traffic during a counter outage; failing closed converts a counter outage into a total outage. Pick one, bound it, for example a local fallback bucket at a fraction of the limit for the duration, and say so in the contract.
- Specify SDK behaviour: honour Retry-After over local backoff, apply full jitter otherwise, never retry 402 or 403, and cap total attempts so retries expire before the caller's own deadline.
Worked solution 25 min
- Write a three-row table of limit, refill behaviour, status code, headers, and the client's correct action.
- Write the exact header set for one throttled response and one quota-exhausted response, showing that they differ.
- Write the documentation sentence that states aggregation across credentials and regions, and the sentence marking Remaining advisory.
- Choose and justify the counter-store failure behaviour, then state the numeric bound it puts on overage or on availability.
- Write the SDK's decision function: given status and headers, return sleep duration or surface to the caller.
Follow-up
- Two services under the same tenant each read Remaining = 50 and each send 50 requests. What did the contract promise, and what actually happens?
- How would you keep a burst from a staging workspace out of the production workspace's share of the same tenant's bucket?
One tenant's counter writes stall the whole connection pool
A change that made a per-tenant usage counter correct now produces site-wide latency whenever one large tenant writes: unrelated endpoints time out waiting for a connection while database CPU stays low and no statement is slow. The change wraps the counter update in a transaction that takes SELECT ... FOR UPDATE on one row, calls an external pricing service, then updates and commits. Give an ordered checklist, the arithmetic that bounds that tenant's write rate, and three repairs with the cost each one accepts.
Approach
- Separate waiting from working. Low database CPU alongside high application latency points at a queue, so instrument connection-acquisition wait separately from query execution time; that queue forms in the application and is invisible in database metrics, which is why the database looks healthy throughout.
- Confirm the lock rather than assuming it: sample waiting sessions and group by wait event, relation and tuple. Contention concentrated on one tuple belonging to one tenant is the signature; a deadlock would instead show the database aborting transactions after its detection timeout, which is not happening here.
- Do the arithmetic out loud. Throughput on a serialised row is one divided by the lock hold time, and the hold spans the external call, so a 20 ms pricing call caps that tenant near 50 writes per second no matter how many pods run. Every waiter also holds a pooled connection while it queues, so the shared pool drains and unrelated tenants fail at acquisition.
- Repair one: shrink the critical section to a single statement with the price resolved before the transaction opens. Cost is a stale price for the duration of one request and a second round trip; benefit is a hold time measured in the database's own execution time.
- Repairs two and three change where the contention lives rather than how long it is held. Sharding the counter into per-(tenant, bucket) rows and summing on read multiplies write throughput by the shard count, at the cost of an aggregate on every read and a shard count you must size against the largest tenant rather than the median. Accumulating in memory and flushing periodically removes the per-write round trip entirely, paid for with a bounded loss window on crash, which is acceptable for a rate limiter and not for a billing counter.
- Contain independently of which repair wins: a separate pool or per-tenant concurrency cap for this write class, a statement timeout low enough that a pathological query dies before it accumulates waiters, and an idle-in-transaction timeout so a stuck client cannot pin a connection and its locks.
Follow-up
- What would a genuine deadlock look like here, which two code paths would produce one, and how does the database's response differ from what you observed?
- If a transaction-pooling proxy sits in front of the database, which of your three repairs changes behaviour, and what stops working that would have worked on a direct connection?
- The counter also enforces a quota. Why is SELECT the count and then INSERT still wrong after you have fixed the contention?
Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Diagnostic, scored before you study anything
- Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
- Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
- Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.
Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Largest gap: find the boundary rather than the subject
- Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
- Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
- Re-attempt one of them from blank four hours later with nothing open.
Deliverable: A sub-skill map with the two blocking sub-skills circled.
Practice prompt ↗Practice prompt ↗03Drill the blocking sub-skill by repeating the shape
- Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
- State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
- Have someone else read your one-sentence rule and find the precondition you left out.
Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.
Practice prompt ↗Practice prompt ↗04Second gap, plus maintenance on the strongest area
- Run the same sub-skill decomposition on the second-largest gap in half the time.
- Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
- Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.
Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.
Practice prompt ↗Practice prompt ↗Worked solution ↗05The gap that is not a skill
- Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
- Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
- Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.
Deliverable: Two recordings with a counted reduction in time-to-first-question.
Practice prompt ↗06Retest under day-one conditions
- Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
- For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
- Write down which single block you would still lose the offer on.
Deliverable: A second scored rubric placed beside the first, with one named remaining risk.
Practice prompt ↗07Full loop under interview conditions
- Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
- Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
- Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.
Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
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.
Resolve a review disagreement over a quota check
A colleague's pull request enforces a per-tenant quota by selecting the current count and then inserting when it is under the limit. You flag it as a race. They reply that the transaction already runs at repeatable read, so the snapshot makes it safe, and the tests pass. Walk through taking that disagreement to a resolution: what you write in the review, what you demonstrate rather than assert, which fix you propose and why, and what you do if they still disagree after all of it.
Approach
- Answer the claim precisely instead of restating your objection, because they have made a specific technical argument. In PostgreSQL, repeatable read is snapshot isolation; this is write skew, which snapshot isolation permits by design. Both transactions read a count that is stable within their own snapshot, insert disjoint rows that the other cannot see, and both commit, so the limit is exceeded by exactly the concurrency.
- Demonstrate rather than cite. Two psql sessions, both BEGIN ISOLATION LEVEL REPEATABLE READ, both select the count, both insert, both commit: it succeeds. Repeat at SERIALIZABLE and the second commit fails with serialization_failure, SQLSTATE 40001. That takes two minutes, ends the argument without anyone conceding a position, and leaves an artefact for the next reviewer.
- Offer the options with their costs rather than a verdict. Serialisable plus a retry loop on 40001 is correct but obliges every caller to retry and degrades under contention. An increment-and-compare on a counter row — update tenant_quota set used = used + 1 where tenant_id = $1 and used < limit returning used — is safe even at read committed, because a blocked updater re-evaluates the WHERE clause against the row version it finally locks, and zero rows returned means full. A unique or exclusion constraint that makes the surplus write fail is the third.
- Name the plausible non-fix explicitly, since it is what usually gets merged instead: folding the count into the insert as insert ... select ... where (select count(*) ...) < limit is still racy under read committed, because the subquery cannot see the other transaction's uncommitted rows. It looks atomic and is not.
- Say what you do if they still disagree: escalate the decision rather than the disagreement. Attach the reproduction, hand it to the service owner or a third reviewer, and state that you will not block the merge if the owner accepts the risk knowingly — and that you want that acceptance written down.
- Close with the general lesson worth leaving in the review thread: a passing suite is weak evidence for a concurrency claim because it runs one request at a time. Ask for a test that runs two.
Follow-up
- Write the counter-row version. Does your answer change if the quota counts child rows rather than a column?
- Under serialisable, who performs the retry, and what does the API client see if the retry also fails?
- This is the third disagreement with the same reviewer this month. What changes in how you review?
Reverse a webhook ordering decision after measuring its cost
You argued for strict per-subscription ordering in webhook-delivery, which means one in-flight attempt per subscription. It shipped. Three months later a single unresponsive endpoint holds one subscription's queue at a six-hour backlog, and two customers report events arriving out of order anyway once their own retries are counted. Describe a decision you reversed: what you originally optimised for, the measurement that changed your mind, what the reversal cost in engineering time and customer change, and how you told the people who had already built on the original guarantee.
Approach
- State the original decision as a trade you made knowingly. Ordering across a network requires a single in-flight attempt per subscription, and its price is head-of-line blocking whenever one endpoint is slow. 'We priced it wrong' is a much stronger opening than 'we did not realise', and it is usually the true one.
- Bring the measurement that flipped it, not the anecdote: backlog age at the ninety-ninth percentile per subscription, the share of subscriptions where one slow endpoint gated an otherwise healthy queue, and the delivery throughput lost to serialisation. A reversal justified by complaints is indistinguishable from a reversal justified by fatigue.
- Name what you learned about the guarantee itself, which is the engineering content of this story. At-least-once delivery means a retried event already arrives after newer ones and the consumer already must be idempotent, so a guarantee the customer has to defend against anyway was never worth what it cost to provide.
- Describe the migration, because reversing a published contract is the hard half and the part candidates skip. Parallel attempts behind a per-subscription flag, a monotonically increasing sequence number added to the envelope so order-sensitive consumers can sort or discard, documentation that states at-least-once and unordered in those words, and a deprecation measured in quarters because the client is a pinned SDK inside a build pipeline you cannot see or redeploy.
- Give the cost in the two currencies that matter: engineer-weeks, and how many customers had to change code. Then say who you told before it shipped rather than in a changelog afterwards, and which large customer you left on the old behaviour and for how long.
- Close with the signal you now weight differently, stated as something you would do earlier next time: measuring the blocking cost on the slowest decile of endpoints before committing to the guarantee, rather than after a customer noticed.
Follow-up
- A customer insists they need ordering. What do you offer them that is not global serialisation?
- How did you choose the deprecation window given that you cannot see or redeploy the clients?
- What would have to be true for you to reverse back?
Disclose a cross-tenant webhook delivery to affected customers
An enqueue path took the subscription from one lookup and the payload from another. For nineteen minutes, webhook_delivery rows were created whose tenant_id did not match the subscription's tenant, and eleven payloads were signed and sent to four endpoints belonging to other customers. You hold payload_digest, delivery timestamps and response codes. Describe how you handle a disclosure of this kind: what the records prove, what they cannot prove, what you say before you know everything, the one code change that closes it, and which parts you personally drove.
Approach
- Bound the population before saying anything externally. The affected set is deliveries in the window where the event's tenant and the subscription's tenant differ; the ones that actually left are those with delivered_at set and a 2xx in last_response_code. Attempted and delivered are two different counts and a disclosure has to use the right one in the right sentence.
- Separate what the records prove from what they do not, and say both halves rather than the flattering one. They prove which payloads were signed, where they went, and — through payload_digest — exactly which bytes. They do not prove what the receiving system did with them, and they do not bound the window more precisely than your deploy timestamps do.
- Communicate on the facts you hold, with the scope stated as an upper bound: 'at most eleven payloads, four recipient endpoints, these fields, this window' is more useful and more honest than waiting a day for certainty. The field list matters more than the event count, because a customer cannot assess exposure from 'an event'.
- Name the code change precisely, because this class never originates in the delivery worker. Compare the event's tenant against the subscription's tenant at enqueue and again immediately before the payload is signed, and make the second comparison drop the delivery rather than log a warning. Say why one check is insufficient: the enqueue check protects against the bug you know about, the pre-signing check protects the boundary itself.
- Run the history question in parallel and say so: a query over historical deliveries for the same mismatch tells you whether this was nineteen minutes or a year, and you would rather find the second case yourself than have a customer find it after your disclosure.
- Split the response into workstreams with owners — recipients asked to delete, affected customers notified, the check landed with a test, history swept — and say which you personally drove and which you handed off. Claiming all four is not credible and claiming none is not ownership.
Follow-up
- The historical sweep finds two more instances from last year. What changes in what you have already told people?
- Who approves the wording, and what do you do when you are asked to soften the scope?
- A customer asks you to prove a redelivery contained the same bytes as the original. What do you show them?
- 01
A colleague's pull request enforces a per-tenant quota by selecting the current count and then inserting when it is under the limit. You flag it as a race. They reply that the transaction already runs at repeatable read, so the snapshot makes it safe, and the tests pass. Walk through taking that disagreement to a resolution: what you write in the review, what you demonstrate rather than assert, which fix you propose and why, and what you do if they still disagree after all of it.
- 02
You argued for strict per-subscription ordering in webhook-delivery, which means one in-flight attempt per subscription. It shipped. Three months later a single unresponsive endpoint holds one subscription's queue at a six-hour backlog, and two customers report events arriving out of order anyway once their own retries are counted. Describe a decision you reversed: what you originally optimised for, the measurement that changed your mind, what the reversal cost in engineering time and customer change, and how you told the people who had already built on the original guarantee.
- 03
An enqueue path took the subscription from one lookup and the payload from another. For nineteen minutes, webhook_delivery rows were created whose tenant_id did not match the subscription's tenant, and eleven payloads were signed and sent to four endpoints belonging to other customers. You hold payload_digest, delivery timestamps and response codes. Describe how you handle a disclosure of this kind: what the records prove, what they cannot prove, what you say before you know everything, the one code change that closes it, and which parts you personally drove.
Is this an official SpaceXAI interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at SpaceXAI. Rounds and questions reflect what candidates have reported, not a process SpaceXAI has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How do SpaceXAI technical screens differ from standard software engineering interviews?
SpaceXAI focuses heavily on practical debugging and hands-on system building rather than abstract LeetCode puzzle solving. Expect live CoderPad sessions where you must run unit tests, identify failing logic, handle exceptions, and implement real data structures like custom caches or transactional stores.
PracHub interview research ↗What is the significance of the "Three Most Exceptional Achievements" form?
For key candidate pipelines, this form acts as a critical evaluation step reviewed by senior engineering leads. It requires you to state your most technically complex achievements in one or two concise sentences, detailing your personal ownership and concrete impact.
PracHub interview research ↗What programming languages are best to use during the interview process?
Python, Go, and Rust are heavily utilized across SpaceXAI engineering teams. While you can often select your language of choice for system design and coding assessments, choosing Python or Go is often advantageous during live debugging rounds where pre-configured test suites are provided.
PracHub interview research ↗How fast does the interview process move from start to finish?
The process moves rapidly. Candidate screens, CoderPad evaluations, and onsite debriefs often yield same-day or next-day feedback. However, final offer packages are subject to executive review before official extension.
PracHub interview research ↗Sources & methodology 3 sources ↗
No official company page is cited. Rounds and questions come from candidate reports and PracHub editorial material; each source shows the date it was read.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
PracHub page · Accessed 2026-09-24 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
PracHub page · Accessed 2026-09-24 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
PracHub page · Accessed 2026-09-24