Interview guides · Akraya
Akraya Software Engineer Interview Guide
Akraya Software Engineer interview guide: reported coding, debugging and client-communication questions, AI screening, take-home tips and a 7-day plan.
The 60-second brief
Akraya works with client companies, and candidates describe Software Engineers as embedded in those clients' project teams. The job reported is to take technical requirements from a client, turn them into maintainable code, and get productive in an existing codebase quickly, either to fill a skills gap or to speed up a product effort. Akraya recruiters and technical screeners handle the early vetting, and candidates who advance may then meet the client's own team. The requirements candidates describe name one modern language (Java, Python and C++ are the examples), Git, and solid data structures and algorithms, with AWS, Azure, GCP, Docker, Kubernetes and CI/CD pipelines as extras.
This guide is for engineers interviewing for a Software Engineer position through Akraya. It covers the five reported technical questions (complexity, optimizing for larger data, linked list versus array, production debugging, maintainable and testable code), the client-facing behavioral topics candidates report, and the early stages: recruiter calls, AI-led assessments and take-home assignments. Because the client differs from one opening to the next, it also shows how to confirm the format for your specific role.
The format varies by client and opening. Candidates describe a sequence that starts with recruiter and technical screeners, can include an AI-led assessment or a take-home with a short turnaround, and may move on to interviews with the client's team.
Focus areas
- Coding Assignments (Take-home)
- AI-Based Pre-screening
- AI Chatbots in Hiring Workflows
Overview
The work described for this role is delivery inside someone else's project. You read a client's specification, ask about the parts that are vague, write code other people will maintain, and keep project managers and client technical leads informed about progress. That shapes what to practise: explaining your reasoning aloud matters as much as producing working code, and the reported questions reflect it. Several ask you to justify a choice (why this approach, why a linked list or an array, how you keep code testable) rather than just produce an answer.
The five reported technical questions split into three groups. Complexity, optimizing a function for a larger data set, and linked list versus array are data-structure and efficiency questions: state the cost in time and space, name the operation that dominates, and tie the choice to how the data is accessed. The production-debugging question is about sequence: how you scope impact, find what changed, mitigate, and then prove a cause in a distributed system. The maintainability question is about concrete habits, such as naming, small functions, dependency seams and unit and integration tests, so prepare examples from your own code instead of slogans.
The early stages deserve their own preparation. Candidates report that recruiter calls count as a formal conversation, that an AI-led assessment can arrive early, and that some loops use a take-home assignment with a tight turnaround. For the AI assessment, find a quiet place and practise giving short structured answers aloud. For a take-home, practise writing down scope, assumptions and an estimate before you open an editor. Candidates' topic lists also include AI chatbots in hiring workflows and handling personally identifiable information, so decide in advance what you will share and when. Contact details and your work authorization status are reasonable to give a recruiter early. Items such as a Social Security number, date of birth or ID documents belong after an offer, and only through an official onboarding portal, never by email, chat or a screening form. If a request for personal data arrives, confirm it is genuine by contacting the recruiter through a phone number or address you already trust, not the one in the message. Candidates also report that recruiters and hiring managers match you to client needs from your resume, so be ready to explain your own contribution to every project on it.
Behavioral preparation centers on the client relationship. The reported example is how you respond when a client changes the requirements halfway through development, and the reported topics are stakeholder management and requirement gathering. Prepare two or three real stories where you clarified an ambiguous request, absorbed a change, or pushed back with a trade-off, each with what you did and what happened as a result.
Where to focus your preparation
The reported stages are grouped in one entry below because the number and order of rounds are not reported. It shows how to prepare for each part.
Preparation focus
Candidates describe a progression that depends on the client and the opening. You typically begin with Akraya recruiters and technical screeners, who handle the first vetting. An AI-led assessment may appear early, and some loops include a coding assignment with a short turnaround. Candidates who advance may then interview with the client's team. Ask your recruiter which of these stages apply, whether coding is live or a take-home, and which language you may use. For the screening calls, expect to walk through your resume project by project. For the technical stages, prepare to explain your complexity reasoning aloud, to compare a linked list with an array for a stated scenario, to walk through debugging a production issue in a distributed system, and to describe how you keep code maintainable and testable. For the AI assessment, answer in a structured way from a quiet room.
What to demonstrate
- Reported topics: Big O reasoning, memory use and the choice of data structure
- Reported topics: code quality, meaning readable naming, modular structure and unit and integration tests
- Reported topics: stakeholder management, clarifying requirements and handling a changed scope
- Reported question: explaining your own contribution to each project on your resume
How to prepare
- Message the recruiter for the stages that apply, whether coding is live or a take-home, and which language to use
- Rehearse the five reported technical questions aloud, giving time and space cost with a one-sentence reason for each choice
- Write a one-page sheet per resume project: your role, the stack, one trade-off you made and the outcome
- Practise short structured answers aloud in a quiet room, since an AI-led assessment may come early
Where candidates lose points
Quoting a Big O figure with no reason behind it
When asked about the complexity of your solution, name the dominant operation, give time and space separately, and say which alternative you rejected and why. For example, say that a hash set trades O(n) extra memory for lookups that avoid a nested loop.
Answering the question about optimizing a function for a larger data set with a trick before you know what larger means
Ask what grows: the number of records, record size, or call frequency. Find the dominant cost, then choose between a better algorithm, a better data structure, batching or caching. Say how you would measure before and after.
Reciting textbook linked list versus array facts without using the scenario
Ask what the scenario does most: random reads, appends, inserts or deletes in the middle, or iteration. Remember that O(1) linked list insertion needs a reference to the node, that arrays have better cache locality, and that linked list nodes carry pointer overhead.
Jumping to a fix when asked to walk through debugging a production issue in a distributed system
Follow a sequence: scope the impact, check what changed recently, mitigate first with a rollback or feature flag, then use logs, metrics and traces tied together by a request id to narrow the cause. Separate the mitigation from the root cause and name the follow-up.
Treating the recruiter call, the AI assessment or your own resume as a formality
Candidates report that recruiters act as your advocate and that AI-led assessments can come early. Prepare for both like a live interview, and be able to explain what you personally built on each project listed.
Questions & practice
Can you describe the time complexity of your solution and why you chose this approach?
Medium · Technical Proficiency and Coding
Approach
- Name the variables first, then give time and extra space separately: n elements, k distinct keys, m queries. For example, a hash-set pass for pair-sum is O(n) time and O(n) space, while sorting plus two pointers is O(n log n) time and needs only the sort's working memory.
- Derive the bound from the code, not from memory. Count the loop nesting and the cost of each call inside it.
x in some_listin Python is O(n), so using it inside a loop makes the whole thing O(n^2).x in some_setis O(1) on average and O(n) in the worst case when hashes collide. - Say which case you are quoting. A dynamic-array append is amortized O(1) because the capacity grows by a constant factor (Java's ArrayList grows about 1.5x, CPython's list about 1.125x), but one append can cost O(n). Quicksort averages O(n log n) and degrades to O(n^2) on bad pivots. Hash lookups are O(1) only on average.
- Justify the choice against the brute force with a number. With n = 10^5, an O(n^2) pair scan is about 10^10 steps and too slow, while O(n log n) is about 1.7 million. If n is tiny and fixed, say the simpler version is fine and why.
- Count the hidden costs. Slicing copies O(k) elements, repeated string concatenation can be O(n^2) in languages with immutable strings, and recursion adds O(depth) stack space, which can overflow on deep inputs.
Follow-up
- Can you get extra space down to O(1), and what does that cost in time?
- How does the bound change if the input arrives as a stream or is already sorted?
- Where would the constant factor matter more than the big-O for this input size?
How would you optimize this function to handle a larger data set?
Medium · Technical Proficiency and Coding
Approach
- Measure before changing anything. Profile the function (cProfile in Python, a JFR or async-profiler capture in Java) and time it at n and 2n. Time roughly doubling means linear; roughly quadrupling means quadratic, so look for a nested loop or a repeated scan.
- Fix the algorithm first. Replace repeated lookups in a list with a hash map or set, which turns O(n*m) into O(n + m). Sort once and binary-search instead of rescanning. Use prefix sums for repeated range totals, and memoize repeated subcalls.
- Bound memory once data outgrows RAM. Stream with iterators or generators instead of materialising lists, and process in fixed-size chunks. For top-k, keep a heap of size k: O(n log k) time, O(k) space. For a full ordering beyond memory, use an external merge sort.
- Fix I/O before you parallelise. Replace N+1 queries with one batched query, push filters into the database where an index can serve them, and cache results that do not change. Add parallelism only when partitions are independent. CPython threads do not speed up CPU-bound work because of the GIL, so use processes.
- Prove it is still correct. Keep the old version as a reference, compare outputs on random and edge-case inputs (empty, duplicates, maximum size), benchmark at the target size, and name the trade-off you accepted, such as more memory for less latency.
Follow-up
- The data no longer fits on one machine. How do you partition it, and what happens to keys that span partitions?
- If you could improve only one of memory or latency here, which would you pick and why?
- How would you stop a later change from bringing the regression back?
What are the trade-offs between using a linked list versus an array in this specific scenario?
Medium · Technical Proficiency and Coding
Approach
- Access: an array reaches index i in O(1) because the address is computed. A linked list must walk i nodes, so it is O(n), and binary search on it gains nothing.
- Insert and delete: in a linked list these are O(1) once you hold the node (a singly linked list also needs the predecessor to delete), but finding the position is O(n). In an array, a middle insert or delete shifts elements in O(n). Appending to a dynamic array is amortized O(1), with an O(n) copy when it resizes.
- Memory and cache: an array is contiguous, so scans are cache- and prefetch-friendly. Each list node pays for one or two pointers (8 bytes each on a 64-bit system) plus its own allocation, and following pointers causes cache misses. That is why arrays often win in practice even with some middle inserts.
- Tie the answer to the scenario. Random access, sorting or binary search point to an array. A queue points to a ring buffer or deque. For O(1) move-to-front and eviction, as in an LRU cache, use a doubly linked list paired with a hash map. If references must stay valid while elements are inserted, use a list, because resizing a C++ vector invalidates its iterators.
- Cover the edge cases in code: an empty structure, updating head and tail when the first or last node changes, a single-element list, and the off-by-one when shifting array elements.
Follow-up
- Implement an LRU cache with O(1) get and put. Which structures would you use?
- Why can a vector beat a linked list on middle insertions in benchmarks, despite the worse big-O?
- How would you detect a cycle in a linked list without extra memory?
How do you ensure your code is maintainable and testable for other team members?
Medium · Technical Proficiency and Coding
Approach
- Separate logic from side effects. Keep the core computation in small pure functions, and pass the database client, HTTP client and clock in through constructors or parameters (dependency injection). Tests can then use fakes instead of real network calls or the system time.
- Test in layers. Write many fast unit tests on the logic, including the edge cases (empty input, null, boundary values, duplicates), and fewer integration tests at the database and API boundaries. Make tests deterministic with a fixed clock, seeded randomness and no shared state, and name each test after the behaviour it checks.
- Make the code readable without its author. Use names that state intent, functions that do one thing, and comments that explain why rather than what. Document public interfaces with their inputs, outputs and errors. Let a formatter and linter (Black and Ruff, ESLint, Checkstyle) settle style so reviews focus on design.
- Enforce it through the workflow. Keep pull requests small, run tests and lint in CI on every push before merge, and require review. Treat coverage as a way to spot untested branches, not a number to hit.
- Back it with one concrete example: a function with a hidden dependency (a direct database call or
datetime.now()) that you refactored to take that dependency as a parameter, the test that became possible, and the bug it caught.
Follow-up
- How do you test code that calls an external API you do not control?
- How would you add tests to a legacy module that has none before changing it?
- When is a mock the wrong tool, and what would you use instead?
Write the update path that detects a concurrent edit
Medium · optimistic concurrency · transactional outbox · audit history
resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.
Approach
- One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
- Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
- State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
- Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.
Follow-up
- A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
- Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?
Denormalise tenant onto revisions and backfill it live
Hard · denormalisation · online migration · backfill
resource_revision (revision_id, resource_id, version, actor_user_id, change_kind, patch, request_id, created_at) has 400M rows and no tenant column; tenant_id lives only on resource. Two reads need it: a tenant-scoped audit feed ordered by created_at DESC, and an offboarding purge. Both join back to resource today. Justify adding tenant_id to resource_revision against those two reads, name the anomaly the copy introduces and the constraint that prevents it, then give the ordered migration for a live table taking 1.2k writes/second — the lock each step takes, how the backfill is batched, and where each step stops being reversible. PostgreSQL 16.
Approach
- Justify from the access path rather than from taste. Without the column, the audit feed either scans resource_revision by created_at and discards other tenants' rows, or resolves the tenant's resource_ids first and probes with them — both proportional to the tenant's whole history rather than to one page. With (tenant_id, created_at DESC, revision_id DESC) it is a seek that stops at 50 rows, and the purge becomes a ranged delete instead of a join.
- Name the cost exactly: a second copy of a fact can disagree with the first. Make the disagreement unwritable rather than documented — add UNIQUE (resource_id, tenant_id) on resource so it can serve as a foreign-key target, then FOREIGN KEY (resource_id, tenant_id) REFERENCES resource (resource_id, tenant_id) on the revision table. A revision can then only ever carry its parent's tenant.
- Step one, expand: ALTER TABLE resource_revision ADD COLUMN tenant_id BIGINT NULL, with no default, so it is a catalogue change and no rewrite. It still needs ACCESS EXCLUSIVE for an instant, and that instant queues behind the longest open transaction on the table while every later query queues behind it — set lock_timeout to 2s and retry rather than wait.
- Step two, dual-write: deploy the writer that populates tenant_id on every new revision while reads still use the join. Reversible by redeploying the previous build, because nothing reads the column yet.
Follow-up
- The backfill is half finished and a rollback is required. What state is the table in, and what does the previous build do with a half-populated column?
- How do you verify the backfill actually finished, given rows are still being inserted while it runs?
Keep one tenant's bulk export from starving projection updates
Medium · backpressure · fair scheduling · head-of-line blocking
The worker fleet runs about 600 jobs/second out of job_run, claimed under a lease and heartbeated while running. Most handlers finish under 200 ms; exports run for minutes. One tenant enqueues 50,000 exports. Design the scheduling so that tenant's exports slow down and nothing else does: the queue split, the concurrency caps and where they are enforced, the claim query, and the backpressure signal that stops accepting new work. State the in-flight worker count each class needs at its arrival rate, and what the cap costs the tenant that hits it.
Approach
- Do the concurrency arithmetic first, because it determines the pool shapes. Required in-flight work is arrival rate times service time: 600/second of 200 ms handlers needs about 120 workers, while one export per second at 180 seconds needs 180 on its own. The long tail dominates any pool it shares, so a single pool sized from the mean is consumed by exports while 50 ms projections queue behind them. That is head-of-line blocking, and adding capacity does not fix it because the ratio is what is wrong.
- Split by job class rather than by priority. Priority inside one pool still lets a running export hold its worker for the next four minutes - there is no preemption for a handler already executing. Separate queues and separate worker processes give each class a floor the other cannot take.
- Enforce the per-tenant cap inside the claim, not at enqueue. Claim with SELECT ... WHERE status='queued' AND run_after <= now() AND job_type=$1 ORDER BY run_after, job_run_id FOR UPDATE SKIP LOCKED LIMIT 1, restricted to tenants currently under their cap. Derive the running count from rows in status 'running' with a live lease rather than from a counter incremented at claim and decremented at completion: the derived count self-heals when a worker dies, and the counter leaks a slot every time one does.
- Make the ordering fair over tenants instead of over jobs. Strict FIFO with a cap still forces the claim to scan past 50,000 rows belonging to a capped tenant before it finds anyone else's work, so cost grows with backlog depth. Pick the tenant first from a small per-tenant queue-depth summary, then claim within that tenant, which makes the scan proportional to the number of active tenants instead.
Follow-up
- An export outruns its lease, so two copies run. What must the handler do to make the second copy harmless, and what does it write to guarantee that?
- With a cap of 4, the tenant's 50,000 exports now take hours. What do you tell them, and does the answer change if they are the largest tenant on the platform?
Design the bulk write endpoint a migration script retries blindly
Medium · batch api · partial failure · idempotency
A customer's migration script pushes 2 million resources through POST /v1/resources:batch, up to 500 items per call, and retries any call that errors or times out. Within a call some items fail validation, some collide with rows that already exist, and some succeed. Specify the request and response shape, whether a batch is atomic or per-item, how idempotency works for the call and for each item, the status code for a mixed outcome, the size and item-count limits with their error codes, and exactly what the script does after a timeout mid-batch.
Approach
- Choose atomicity deliberately and price it. All-or-nothing means one transaction holding locks for the whole batch on a primary already absorbing about 1.2k writes/second, which bounds batch size by lock duration, and it turns one bad row into 499 rejections the script must resend. Per-item partial success is the right default for a migration, and the contract's job is then to make a partial outcome impossible to miss.
- Require a client-supplied id on every item and echo it in every result. Deriving a per-item key from the array index breaks the first time the script resends a batch with the failures removed: the indices shift, previously-succeeded items acquire new keys, and they are created a second time.
- Key the effects at two levels. The call's Idempotency-Key covers an exact resend of the same bytes; per-item keys of (tenant_id, client_item_id) make a partially-applied batch safe to resend whole. Resending an identical batch must reproduce the same per-item results, not 500 conflicts the script has to interpret.
- Answer a mixed outcome with one status plus per-item detail: 200, or 207 borrowed from WebDAV if you prefer it - document whichever you pick - carrying an array of client_item_id, per-item status, and either resource_id or an error code from the same taxonomy the single-item endpoint uses. Reserve 4xx for the request as a whole: unparseable body, too many items, payload over the limit (413). Put a failed count at the top level so that even a script checking only the cheapest thing cannot conclude success while rows were dropped.
Follow-up
- At 500 items per call, what is the wall-clock time for 2 million rows, and what pacing do you publish so the migration does not become an incident?
- One item in every batch fails with the same code. How does the script discover that without a human reading logs?
Walk me through your process for debugging a production issue in a distributed system.
Medium · Technical Proficiency and Coding
Approach
- Confirm the symptom and its scope before looking for a cause. Check the error rate, the latency percentiles (p50 and p99) and resource saturation. Establish the blast radius: which services, regions and customers. If users are affected, mitigate first by rolling back the last deploy, turning off a feature flag or shedding load.
- Ask what changed. Put deploys, config and flag changes, dependency upgrades, traffic shifts and infrastructure events on one timeline next to the moment the metric moved. Say whether the change is the trigger or the actual cause.
- Follow a single failing request. Use a correlation or trace ID through structured logs and distributed tracing (OpenTelemetry with a backend such as Jaeger) to find the first hop where latency or errors appear. A slow downstream service often shows up as timeouts in its callers. On Kubernetes,
kubectl describe podshows OOMKilled restarts andkubectl logs --previousshows the crashed container's output. - Check the failure modes specific to distributed systems: retry storms with no backoff or jitter, exhausted connection pools, timeouts longer than the caller's timeout, non-idempotent retries that write duplicates, clock skew breaking ordering, stale caches, queue consumer lag and partial network partitions.
- Prove the hypothesis before you call it fixed. Reproduce it in staging or with replayed traffic, or add targeted logging. Ship the fix and confirm the metric recovers. Then write a blameless postmortem with concrete actions: an alert on the leading signal, a regression test and a runbook entry.
Follow-up
- It affects 1% of requests and you cannot reproduce it locally. What do you do next?
- How would you tell whether retries are making the incident worse?
- What instrumentation would you add so the next one is found faster?
Suggested practice checklist
The plan follows the reported topics in order: format and resume first, then complexity and optimization with a timed session on unseen problems, data-structure trade-offs, production debugging, maintainable code with Git habits and take-home scoping, client-facing stories, and a full dry run.
Day 1 — Confirm the format and inventory your resume
- Write to your recruiter asking which stages apply (recruiter call, AI-led assessment, take-home, client interview), whether coding is live, and which language you may use.
- For every project on your resume, write what you personally built, the stack, one trade-off you made and the result.
- Write a short spoken summary of your background and what kind of client work you want.
Deliverable: A list of format questions sent to your recruiter, plus one project sheet per resume entry.
Day 2 — Complexity and optimizing for larger data
- Take three problems you have solved in Java, Python or C++. For each, write the brute-force version and an improved one, with time and space cost and a one-sentence reason for the choice.
- Take a function with a nested loop, such as finding duplicates. Time it on a small input and one that is 100 times larger, then rewrite it with a hash set or a sort.
- Run a timed session on two problems you have not seen, 25 minutes each, narrating your approach aloud as you code. When each is done, state its time and space cost and one trade-off.
- Say your answers aloud as you would explain them to a client technical lead.
Deliverable: Three complexity statements with reasons, one before-and-after timing, and notes on how the two unseen problems went.
Day 3 — Linked list versus array, scenario by scenario
- Implement a singly linked list (insert at head, delete by value, reverse) and use a dynamic array in the same language.
- Measure traversal and middle insertion on a large input for both, and note the effect of memory layout.
- Write a four-row table of scenarios (queue, random lookup, frequent head inserts, a cache with eviction) with the structure you would pick and the reason.
Deliverable: Benchmark numbers for both structures and the scenario table with a one-line justification per row.
Day 4 — Debugging a production issue in a distributed system
- Write a short debugging sequence: scope impact, check recent changes, mitigate, then use logs, metrics and traces correlated by request id to find the cause.
- Walk through a scenario aloud, such as intermittent timeouts after a deploy across several services, and state what each check rules out.
Deliverable: A one-page debugging checklist and a recorded spoken walkthrough of one scenario.
Day 5 — Maintainable, testable code, Git habits and take-home scoping
- Take a messy function of thirty lines or more. Split it into small pure functions, inject its dependencies, and write unit tests for its edge cases plus one integration test.
- Make that refactor on a branch as a sequence of small commits with clear messages. Be ready to explain how you would resolve a merge conflict, and how you would use git bisect to find the commit that introduced a regression.
- Write a scope note for an imagined take-home: assumptions, what is in and out, and an estimate with its main unknowns.
- Rehearse the explanation of why your changes make the code easier for a teammate to maintain.
Deliverable: The refactored function with its tests, a branch with a readable commit history, and a one-paragraph scope and estimate note.
Day 6 — Client-facing stories
- Write a story for the reported prompt on a client changing requirements midway through development: what you clarified, what you traded off, how you communicated it, and the result.
- Write a second story about conflicting feedback or requirements from two stakeholders and how you resolved it.
Deliverable: Two written stories and a list of five clarifying questions.
Day 7 — Full dry run
- Record yourself in a quiet room answering the five reported technical questions aloud in structured form.
- Replay it and note any answer that lacked a stated reason, a trade-off or an edge case.
- Run a final recruiter-call rehearsal: your background, your resume projects and the questions you would ask about the client role.
Deliverable: One full recording with self-review notes and a final list of questions for the recruiter.
Client-facing and delivery questions
Candidates report that the behavioral side of Akraya loops covers stakeholder management, such as handling conflicting requirements or feedback, and requirement gathering, such as clarifying expectations before a project starts. For a client-embedded role, build each story around what you asked, what you decided, how you told people, and what happened next.
- How do you respond when a client changes the project requirements halfway through development?
- Describe a time two stakeholders gave you conflicting requirements or feedback, and how you resolved it.
- Tell me about a project where the request was ambiguous. What did you ask before you started, and how did the answers change your plan?
- Walk me through a project on your resume and say exactly which parts you built.
- Describe a time you had a very short turnaround on an assignment. How did you scope it and what did you cut?
Ship under a deadline and bound the debt you chose
Medium · pagination · technical debt · tradeoffs
You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.
Approach
- Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
- Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
- Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
- Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.
Follow-up
- At what page depth does the offset version breach your latency budget, given your page size and row counts?
- What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?
Narrate an outage you owned from page to postmortem
Hard · incident response · blast radius · postmortems
Pick an incident you personally drove, ideally one where writes were affected rather than reads. In six to eight minutes: state the symptom as it first appeared on a dashboard, the blast radius you established before you knew the cause, the mitigation you applied and when, the mechanism you eventually proved, and the follow-up that would prevent a repeat. Bring numbers: error rate, tenants affected, minutes to mitigate, minutes to resolve. If you cannot name what you measured, choose a different incident.
Approach
- Open on the signal rather than the cause: which metric at which percentile moved, on which service, at what time, so the listener follows the same evidence you had rather than a conclusion you already reached.
- Separate mitigation from diagnosis out loud. State what you did to stop the bleeding (flag off, shed traffic, drain a lease, roll back a deploy) and say plainly that you did it before the mechanism was known, because those are two jobs with different deadlines.
- Establish blast radius in countable terms: how many tenants, how many writes, and crucially whether the effect was loss or only delay. An append-only revision table or a pending outbox row means the change survived and the projection was merely behind, which is a repair rather than a data-loss incident.
- Prove the mechanism instead of asserting it. Name the trace span that grew, the plan that flipped to a sequential scan, the lease that expired, plus one alternative you ruled out and the signal that stayed flat while you ruled it out.
Follow-up
- What would you do differently in the first five minutes, given the same dashboard and no more information?
- Which follow-up action did you deliberately not take, and why was dropping it the right call?
Estimate work you have never done and defend the range
Hard · estimation · backfills · expand-contract
You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.
Approach
- Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
- Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
- Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
- Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.
Follow-up
- How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
- Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?