Interview guides · A Place for Mom
A Place for Mom Software Engineer Interview Guide
A Place for Mom Software Engineer interview guide: TypeScript, NestJS, React and event-driven design questions, with a seven-day plan.
The 60-second brief
A Place for Mom runs a platform that helps families find senior care. Reports place engineers on the consumer web experience, the software advisors use, and AI voice and chat agents, with search-driven discovery and provider matching underneath. The reported stack is a Next.js front end, NestJS on Node for APIs, Postgres for data, Redis or Kafka for events and queues, and an Agentic Platform that lets teams ship AI features safely. Reports describe the Software Engineer role as spanning those layers: web pages and APIs, event-driven services behind engagement and messaging, and the points where AI providers plug in.
This guide is for Software Engineer candidates at A Place for Mom, across frontend and backend work. It covers the seven reported questions (five hands-on implementation tasks and two system design prompts), the evaluation areas candidates describe, a loop section that is candid about how little is reported on round format, a behavioral section, and a day-by-day plan. It does not cover other roles at the company.
No fixed round list has been reported. Candidates describe a progression from a recruiter conversation to technical and cross-functional sessions covering TypeScript coding, system design, observability and AI safety, and behavioral alignment. Confirm the real format with your recruiter.
Focus areas
- JavaScript
- TypeScript
- React
Overview
The reported work is full-stack TypeScript with a platform flavour. On the web side it is Next.js and React, with performance and accessibility called out. On the server side it is NestJS APIs, Postgres schemas and indexes, and event-driven services built on queues or streams such as Kafka and Redis. Candidates also describe an Agentic Platform where engineers add provider abstractions, safety guardrails and evaluation hooks. The five reported implementation tasks mirror that spread: a NestJS endpoint, a React refactor, a schema-validated API wrapper, a streaming parser and a retrying provider call. Prepare for all of them in one language, TypeScript, with tests written alongside the code.
For the implementation tasks, depth in the language matters more than breadth. Be ready to talk through generics, type narrowing, utility types, interfaces versus type aliases and strict compiler settings, because the typed-wrapper question rests on them. On NestJS, know modules, dependency injection, pipes, guards and exception filters, plus how a validation pipe turns a request body into a checked object. On React and Next.js, know what triggers a re-render, how memoisation helps and when it does not, the server versus client component split, and the semantic HTML, labels and focus handling that make a component accessible. Reports also list streaming responses, structured logging, feature flags and SSR caching as less common topics.
The design questions are about contracts and failure. The event-driven pipeline for matching families to providers needs a clear event schema, a partition key, idempotent consumers, a retry path, a dead-letter queue and a way to measure the SLA. The monolith-to-service migration needs a versioning scheme, a traffic-shifting plan and a rollback. Candidates also report observability prompts: what to log, trace and alert on, how to cut a P95 latency regression without raising errors, and how to build a redaction layer so personal data never reaches monitoring tools. Practise saying the failure mode out loud for every component you draw.
Sensitive data and AI quality run through the reports. Candidates are told to treat personal and senior-care data with extra care in every example, to describe guardrails, audit trails and failure modes, and not to treat manual spot checks as enough for AI output quality. Scenarios are described as centred on family and advisor experience: faster first conversations, better search and personalisation, steadier voice and chat agents. Bring one system you owned in detail, with the metrics you moved and what you would change, and tie your trade-offs to a user outcome you can defend with real numbers.
Where to focus your preparation
Only one preparation stage is available because no round sequence has been reported in detail; treat it as a checklist for what to confirm with your recruiter and what to practise.
Preparation focus
Candidates do not report a numbered list of rounds, round lengths or an order of questions. What is described is a coding component in TypeScript (backend endpoints, React work, API wrappers, streaming, retries), a system design component (event-driven pipelines, monolith migration, API contracts), discussion of observability and AI safety, and behavioral conversations with cross-functional themes. Start by asking your recruiter for the sequence, which stages involve writing code, and what environment you will use. Then prepare every area: write code with tests and error handling, narrate your trade-offs, and keep a reusable architecture diagram you can adapt on the spot.
What to demonstrate
- Working TypeScript with tests and explicit error handling for the five reported implementation tasks
- API and event-driven design with clear contracts, retries, dead-letter handling and rollout plans
- Observability, privacy and product-linked reasoning: what you log, trace and alert on, how personal data is kept out of tools, and which user outcome a choice serves
- AI-feature safety: handling non-deterministic output, building an evaluation set, and guardrails and rollback for an Agentic Platform feature
How to prepare
- Ask the recruiter which stages involve live coding, which involve design, and whether a take-home or code sample is expected
- Complete the NestJS pagination endpoint, the typed API wrapper and the retry wrapper end to end, each with its own tests
- Draw the provider-matching pipeline and the monolith migration once each, naming the failure mode of every box, and prepare one system you owned with its metrics and an incident lesson
- Outline one AI feature end to end: a small evaluation set with regression tracking, guardrails that keep personal data out of prompts and traces, provider failover, and the trigger for rolling it back
Where candidates lose points
Returning an unbounded, unvalidated list endpoint
For the NestJS pagination question, cap the page size, validate every query parameter and filter value through a DTO and a validation pipe, choose a stable sort with a tiebreaker such as the id, and cover the bad-input paths in your tests, not just the happy path.
Casting an API response to a type and calling it validated
A cast such as as Provider checks nothing at runtime. Parse the response with a schema library, return a typed result or a typed error, and say what happens when the provider adds, drops or renames a field.
Retrying everything the same way, with no jitter and no limit
Retry only transient failures (network errors, 408, 429, 502/503/504, and 500 only for idempotent operations), respect Retry-After, add jitter and a maximum attempt count, set a timeout per attempt, and state that non-idempotent calls need an idempotency key before they are retried at all.
Drawing an event pipeline with no answer for duplicates, ordering or poison messages
Name the partition key and what ordering it preserves, make consumers idempotent, route repeated failures to a dead-letter queue with an owner and a replay path, and say which metric tells you the matching SLA is being missed, for example consumer lag or message age.
Instrumenting with raw payloads and leaking personal data into logs or traces
Reports stress care with personal and senior-care data. Log identifiers and metadata only, redact at a single boundary before data reaches observability tools, and describe how you would prove the redaction works with a test that asserts forbidden fields never appear.
Questions & practice
Refactor a React component to remove unnecessary re-renders and improve accessibility.
Medium · Coding and TypeScript
Approach
- Measure before changing anything: record the slow interaction in the React DevTools Profiler with highlight updates on and the option that records why each component rendered. Memoizing components whose renders cost nothing adds complexity without a gain.
- Find the usual causes: state held higher than the components that read it, a context value object recreated on every render, and inline objects, arrays or arrow functions passed to memoized children. Fix in order of preference: move state down or pass expensive subtrees as children, split or memoize the context value, then React.memo on the expensive child with useCallback/useMemo for its props.
- Remove derived state: a filtered list or a computed label belongs in render (wrapped in useMemo only if it is expensive), not copied into state and synced with useEffect, which costs an extra render and can go stale. Use stable keys from the data, not array indexes, for lists that reorder.
- Fix accessibility at the element level: replace clickable divs with <button> (focusable, and Enter and Space activate it natively), associate every input with a <label>, give icon-only buttons an accessible name, reflect open/closed state with aria-expanded, move focus into a dialog on open and back to its trigger on close, and announce async results through an aria-live region.
- Lock both changes in with tests: React Testing Library queries by role and name (getByRole('button', { name })), user-event for keyboard paths, an axe check such as jest-axe, and a render-count or Profiler assertion on the hot path so the regression cannot return unnoticed.
Follow-up
- When does React.memo cost more than it saves, and how would React Compiler change this refactor?
- The list renders 5,000 rows. What do you do beyond memoization?
- How would you make a custom select or combobox usable by keyboard and screen reader?
Write a streaming data handler that safely parses partial payloads and back-pressure signals.
Medium · Coding and TypeScript
Approach
- State the framing first: chunk boundaries never line up with message boundaries. For newline-delimited JSON, keep a carry-over buffer, append each chunk, split on '\n', parse every complete line and keep the trailing fragment for the next chunk; for server-sent events, split events on a blank line and read the data: lines.
- Decode bytes with a streaming decoder (TextDecoder's decode(chunk, { stream: true }) or Node's StringDecoder) so a multi-byte UTF-8 character split across two chunks is not corrupted. On end, flush the decoder and handle the final fragment: parse it, or report a truncated message.
- Bound memory: cap the carry-over buffer at a maximum message size and fail the stream with a clear error when a line exceeds it, otherwise a peer that never sends a delimiter grows memory without limit. Scan only newly appended bytes for delimiters so total work stays linear in input size.
- Respect back-pressure: writable.write() returns false once its buffer passes highWaterMark, so stop reading until 'drain'. The simplest correct forms are a Transform with readableObjectMode joined by pipeline() from 'node:stream/promises', which propagates errors and destroys every stage, or a for await loop over the readable that awaits each async handler so reading pauses while work is slow.
- Decide the error contract per message: a malformed line is either counted, logged without its content and skipped, or it fails the stream; say which and why. Propagate cancellation upstream with an AbortController when the client disconnects, and test by splitting the same payload at every byte offset, including inside a multi-byte character.
Follow-up
- A single JSON value is tens of megabytes. How do you parse it without holding it all in memory?
- The consumer is a slow database writer. How do you batch writes without defeating back-pressure?
- The client closes the connection mid-stream. What must be cleaned up, and how do you test it?
Merge partitioned event streams into one ordered feed with bounded lateness
Hard · k-way merge · watermarks · out-of-order streams
The read-model service consumes 64 log partitions carrying about 4,000 events per second in total. Each partition is ordered within itself, but partitions drift by up to 30 seconds, and the activity feed must present a tenant's events in occurred_at order. Produce the merge. State its complexity, the buffer it requires in events and in bytes, what happens when one partition is idle, and what you do with an event that arrives after you have already emitted its position. Payloads average 1 KB.
Approach
- Merge with a min-heap over the 64 partition heads keyed on (occurred_at, event_id): O(log P) per event and O(n log P) overall. The tie-break on event_id is what makes the output deterministic when two partitions carry the same millisecond, which matters because the feed is paginated and a non-deterministic order reorders pages under the reader.
- Emitting the heap head is only correct once every partition has produced everything up to that timestamp, so the emit condition is a watermark: the minimum across partitions of the highest occurred_at seen, less the allowed lateness. Events are held until the watermark passes them, which is what turns individually ordered streams into a jointly ordered one.
- Size the buffer from the lateness rather than guessing: 4,000 events per second times 30 seconds is 120,000 buffered events, and at 1 KB each about 120 MB of heap. That number is the real price of the ordering guarantee and belongs in front of whoever asked for it.
- Handle the idle partition explicitly, because it fails the feed rather than corrupting it: a partition with no traffic never advances its own maximum, so the watermark freezes and output stops entirely. Either every partition emits a periodic idle marker carrying the broker's current time, or the watermark falls back to wall clock for a partition silent beyond a threshold.
Follow-up
- The lateness budget is raised to five minutes. What is the new buffer, and what besides memory changes?
- The consumer restarts. Where does it resume from, and what does the feed look like for the first 30 seconds?
Find version gaps and relay lag with window functions
Medium · window functions · event tables · frame semantics
outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.
Approach
- Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
- Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
- Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
- Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.
Follow-up
- Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
- Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?
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?
Build a NestJS endpoint that supports pagination, filtering, and input validation; include tests.
Medium · Coding and TypeScript
Approach
- Model the query as a DTO class: limit with @IsInt, @Min(1), @Max(100); optional status with @IsIn over an allowed list; sort restricted to a whitelist of columns. Register ValidationPipe with transform: true (plus @Type(() => Number) or enableImplicitConversion, since query-string values arrive as strings) and whitelist: true, forbidNonWhitelisted: true, so bad input fails with a 400 before the service runs.
- Prefer keyset (cursor) pagination over OFFSET: OFFSET n makes Postgres read and discard n rows, so deep pages slow down and rows shift when inserts land. Order by (created_at, id) for a unique total order, query WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT limit + 1, and use the extra row to set hasNextPage. Return the cursor as an opaque encoded token.
- Back the filter plus sort with a composite index whose leading columns are the equality filters and whose trailing columns are the sort keys, for example (status, created_at, id). Build the WHERE clause only from whitelisted fields with bind parameters, never with string-concatenated values.
- Unit-test the service with Test.createTestingModule and a mocked repository; add e2e tests with supertest against an app using the same global pipes: limit=0 and limit=1000 return 400, an unknown query field returns 400, a tampered cursor returns 400 rather than 500, an empty result returns an empty list with no cursor, and walking every page returns each row exactly once even when created_at values tie.
Follow-up
- The product wants a total result count and page numbers. What does that cost in Postgres, and what would you offer instead?
- How does your cursor behave if the client changes the sort or filter between pages, or the row it points at is deleted?
- Where would you enforce authorization so a caller cannot page through records they should not see?
Implement a typed wrapper that enforces schema-validated responses from a third-party API call.
Medium · Coding and TypeScript
Approach
- Treat the network boundary as unknown: response.json() is typed Promise<any>, and a cast such as
as ProviderResponseis erased at compile time and checks nothing. Define the schema once at runtime with a library such as Zod and derive the static type with z.infer, so the type and the check cannot drift apart. - Signature: async function callApi<S extends z.ZodTypeAny>(req, schema: S): Promise<Result<z.infer<S>>>, where Result is a discriminated union { ok: true; data } | { ok: false; error }. Use schema.safeParse rather than parse, so a validation failure is a value the caller must handle instead of an exception it can forget.
- Give the error a kind the caller can branch on: network (fetch rejected, or timed out via AbortSignal.timeout), http (res.ok false, keep the status), parse (res.json() threw on a non-JSON body) and validation (keep the Zod issue paths). Log issue paths and the provider name, not payload values, so personal data never reaches the logs.
- Choose strictness on purpose: z.object strips unknown keys by default, which tolerates a provider adding fields, while .strict() rejects them and breaks on harmless additions. Be strict on the fields the code reads, use .catch() fallbacks only where a default is genuinely safe, and coerce dates and numbers explicitly at the boundary.
- Test with recorded fixtures: a valid body, a missing required field, a wrong type, an extra field, a 500 with an HTML body, malformed JSON and a timeout, each asserting the exact error kind returned.
Follow-up
- The provider adds a new value to an enum the code switches on. What happens in production with your schema, and what should happen?
- Validating a large response on every call shows up in profiles. How would you reduce the cost?
- Would this wrapper throw or return errors, and how does that choice interact with a retry layer above it?
Add robust error handling and retries with exponential backoff to a provider integration.
Medium · Coding and TypeScript
Approach
- Classify failures before retrying anything: network errors (ECONNRESET, timeouts), 408, 429 and 502/503/504 are usually transient; 400, 401, 403, 404 and 422 will fail the same way again, so return them immediately. Treat a 500 as retryable only when the operation is safe to repeat.
- Use exponential backoff with full jitter: delay = random(0, min(cap, base * 2^attempt)), which spreads clients out so they do not retry in lockstep after an outage. Honor a Retry-After header when present (it may be seconds or an HTTP date), and bound the call with a maximum attempt count, an overall deadline and a per-attempt timeout via AbortSignal.
- Make retries safe: GET and PUT are idempotent by HTTP semantics, POST is not. If the provider accepts an idempotency key, generate it once per logical operation and send the same key on every attempt; if it does not, retry a create only after checking whether the first attempt took effect.
- Stop retry amplification: add a circuit breaker that opens after consecutive failures and fails fast until a half-open probe succeeds, and keep retries at one layer, since three layers each making three attempts can send up to 27 requests for one user action.
- Make it observable and testable: log provider, attempt number, status, chosen delay and a correlation ID (never request bodies), emit retry-count and breaker-state metrics, and unit-test the schedule with fake timers and an injected random source so delays are deterministic.
Follow-up
- The provider times out on a create call. Did the record get created, and how does your code find out?
- The provider recovers from an outage and thousands of pending calls fire at once. What protects it?
- Should this retry live in the HTTP client, or should the work move to a queue with its own retry and dead-letter handling?
Design an event-driven pipeline for matching families to providers with SLAs and DLQ handling.
Medium · System Design / Architecture
Approach
- Fix the contract and the SLA first: the input event (inquiry id, schema version, care needs, location), the output (a ranked list of provider matches) and an end-to-end target stated as a percentile from event creation to matches available, measured from timestamps carried in the event itself.
- Publish reliably with a transactional outbox: write the inquiry and an outbox row in one Postgres transaction and let a relay publish outbox rows to Kafka or Redis Streams, closing the gap where the row commits but the event is lost. Pick the partition key by the ordering you need: family id if events across a family's inquiries must stay ordered, inquiry id if per-inquiry order is enough. A coarser key preserves more order but concentrates load, so one very active key can lag a single partition while the others sit idle.
- Assume at-least-once delivery and make the matching consumer idempotent: record processed event ids, or upsert results under a unique constraint on (inquiry_id, provider_id), so a redelivery creates no duplicate matches or notifications. The consumer filters candidates (care type, location, availability), ranks them, writes results and emits a matches-ready event for the UI.
- Handle failures in two lanes: retry transient errors with backoff (in-process or via retry topics), and send poison messages such as schema failures straight to a dead-letter topic with the error, attempt count and original offset. Kafka has no built-in DLQ, so this is application code; retry topics also give up per-key ordering. Alert on DLQ depth and build a redrive tool.
- Watch the SLA through leading indicators: consumer-group lag, processing-latency percentiles and DLQ rate, with alerts tied to the end-to-end target. Since payloads carry care needs and contact details, keep personal fields out of logs and restrict access to and expire what sits in the DLQ.
Follow-up
- Traffic doubles and consumer lag climbs. How far can you scale consumers, and what caps the parallelism?
- A provider's availability changes after matches were sent. How does the pipeline correct them?
- A bad deploy routes thousands of events to the DLQ. How do you redrive them without duplicate notifications?
Propose a migration from a monolith to a service with versioned APIs and rollout strategy.
Medium · System Design / Architecture
Approach
- Use the strangler pattern: put a routing layer (gateway or reverse proxy) in front of the monolith, extract one bounded context with few joins into the rest of the schema, and move traffic route by route instead of in one cutover. Choose the seam by data ownership, because shared tables are the hardest part to split.
- Write the contract before the code: an OpenAPI spec for v1 that matches current behaviour, consumer-driven contract tests (for example Pact) for each caller, and a versioning rule: additive fields stay in v1, while removing or renaming a field or changing its meaning requires v2, with v1 kept through a deprecation window signalled by a Sunset header.
- Move the data in steps: the service gets its own tables, a backfill copies history, and change data capture or an outbox keeps the copy in sync while the monolith still writes. Avoid ad hoc dual writes from application code, which drift on partial failure, and run reconciliation (row counts, per-key checksums) before switching the write path.
- Roll out gradually: shadow reads to the new service and diff responses against the monolith without serving them, then canary a small share of traffic behind a flag, widening only while error rate and p95 latency stay within the SLO, with rate limiting at the gateway and automated rollback on an SLO breach.
- Plan the rollback for after the service owns writes: if traffic flips back, the monolith must see those writes, so keep syncing changes back until the old path is retired. Retire v1 only when per-client traffic metrics show no remaining callers.
Follow-up
- One monolith transaction updates both the extracted data and data that stays behind. How do you keep them consistent after the split?
- Months after the deprecation notice, one internal caller still uses v1. What do you do?
- How do you trace a single user request across the monolith and the new service during the migration?
One log partition stops advancing while the others drain
Easy · poison message · head-of-line blocking · consumer lag
Search results for a subset of tenants are hours stale; the rest are current. The projection consumer reports lag of zero on 15 of 16 partitions and 400,000 on one. Its error rate is flat and its CPU is idle. outbox_event has no pending rows older than a second, so the relay has published everything it holds. Identify the mechanism, give the ordered checks, and state what you do in the first ten minutes versus what you change permanently.
Approach
- Read the lag distribution first. A slow consumer lags everywhere; zero on fifteen partitions and 400,000 on one is not throughput. Idle CPU on the stuck partition means the consumer is not advancing its offset at all, which points at one message it cannot get past rather than at a rate problem.
- Exonerate the producer before touching the consumer. No pending outbox rows older than a second means the relay published, so the event exists in the log. This separates never sent from sent and never applied, which are different code paths and usually different owners.
- Read the message at the stuck offset and the handler's log lines for its event_id. A flat error rate with no progress has two explanations and you must distinguish them: the handler is throwing and the retry loop is swallowing it, or the handler is blocking on something and never returning. Idle CPU with no error lines favours the second.
- Mitigate before diagnosing further. Move the offending event to a dead-letter store and commit the offset past it. Adding consumers does nothing here, because a partition is consumed by exactly one member of the group, and the blast radius is every aggregate hashed to that partition, not only the aggregate that produced the bad event.
Follow-up
- The dead-lettered event carried aggregate_version 7 and the projection had applied 6. What must the replay do differently if 8 and 9 landed in the meantime?
- How do you show staleness to the user while the partition is behind, given the API already returns the projection's watermark?
Suggested practice checklist
The plan follows the reported question set: TypeScript and backend implementation first, React, web performance and streaming next, then the two design questions, observability and AI platform safety, then behavioral stories and a full mock.
Day 1 — Confirm the format and refresh TypeScript
- Write to your recruiter asking for the stage sequence, which stages involve live coding, the coding environment and whether any take-home or code sample is expected.
- Rework a small module covering generics, type narrowing, utility types such as Pick, Omit and Partial, and strict compiler options; write down one example of each.
- Set up a NestJS project with Jest and supertest so day 2 starts with a working test harness.
Deliverable: A saved recruiter reply, a one-page TypeScript cheat sheet with one example per feature, and a NestJS project that runs one passing test.
Day 2 — NestJS endpoint and typed API wrapper
- Build the paginated, filterable endpoint with a DTO, a validation pipe, a capped page size, a stable sort and an error response for bad input; add tests for valid, invalid and empty-result requests.
- Decide cursor versus offset pagination and write two sentences defending your choice, including what a page boundary does when rows are inserted.
- Write a generic function that calls a third-party API, parses the result with a schema and returns a typed success or a typed failure; test it with a malformed payload.
Deliverable: A tested NestJS endpoint, a written pagination decision, and a tested schema-validated wrapper.
Day 3 — React re-renders, accessibility and Core Web Vitals
- Take a component that re-renders too often, find the cause with the React profiler, and fix it with stable props, memoisation or state moved closer to where it is used; record before and after render counts.
- Audit the same component for accessibility: semantic elements, labels for inputs, keyboard operation, focus handling and announcements for dynamic updates.
- Write a short note on which parts would be server components and which client components in Next.js, and why.
- Measure LCP, INP and CLS on one Next.js page with Lighthouse or the web-vitals library, and name one fix for each: next/image with priority for the LCP image, less client JavaScript or cheaper hydration for INP, and reserved width and height for CLS.
Deliverable: A refactored component with before and after render counts, an accessibility checklist applied to it, a server versus client split note, and a Web Vitals measurement with one fix per metric.
Day 4 — Streaming parser and retry wrapper
- Write a handler that reads a chunked stream, buffers an incomplete trailing record across chunks, parses complete records, and stops reading when the consumer signals back-pressure; test with payloads split mid-record.
- Write a retry wrapper with exponential backoff and jitter, a maximum attempt count, a per-attempt timeout and a classifier for retryable versus fatal errors; test with a fake provider that fails twice and then succeeds.
Deliverable: A tested streaming handler, a tested retry wrapper, and a worked solution to the stream-merging practice question.
Day 5 — Event-driven provider-matching pipeline
- Design the pipeline for matching families to providers on a whiteboard: event schema, topics, partition key, consumers, retry path, dead-letter queue, SLA metric and the real-time read model for the UI.
- Say how a duplicate event, an out-of-order event and a poison message are each handled, and how a replay from the dead-letter queue stays safe.
Deliverable: One annotated diagram with a failure mode for every component, plus worked answers to the three practice questions.
Day 6 — Monolith migration, observability and AI platform safety
- Outline the migration of one monolith endpoint into a service: route-level traffic shifting, versioned API contract, shadow comparison, rollback trigger and the data ownership boundary.
- Write the instrumentation plan for a new feature: structured log fields, trace and correlation identifiers, SLIs, alerts tied to user impact, and a redaction layer that keeps personal data out of monitoring tools.
- Design an API for a long-running agent task (Agent-as-a-Service): a create call that returns a job id, status by polling or SSE, a cancel endpoint, and retries guarded by an idempotency key so a resubmitted request does not start a second run.
- Sketch a provider abstraction that handles provider churn with failover, then build a small evaluation set with regression tracking against a baseline and a rollback trigger, and show how personal data is kept out of prompts and traces.
Deliverable: A migration outline with rollback criteria, an instrumentation plan with redaction rules, an agent-task API contract, and a provider abstraction with an evaluation set and rollback rule.
Day 7 — Behavioral stories and a full mock
- Write bullet outlines for the stories under the behavioral prompts, each with a situation, your action, a measured outcome and what you would change.
- Run one full mock: an implementation task from days 2 to 4, a design from days 5 and 6, then two behavioral stories, and note two questions to ask your interviewers.
Deliverable: Six story outlines, three practised behavioral answers, a completed mock and two questions to ask.
Stories to prepare: ownership, reliability and sensitive data
Candidates describe conversations about systems they owned, metrics they moved, reliability work, responsible handling of sensitive data, and collaboration with Product, Marketing or SEO, Data and Design. No specific behavioral questions have been reported, so prepare stories rather than scripts, each with a measurable outcome and an honest account of what went wrong.
- Walk through a complex system you owned: the architecture, the metrics you moved, an incident or postmortem, and what you would change now.
- Tell me about a time you handled sensitive or personal data in production. What guardrails, audit trail and failure modes did you design, and what did you deliberately not log?
- Describe a latency or reliability regression you diagnosed. What did you measure first, which lever fixed it, and how did you confirm error rates did not rise?
- Tell me about a time Product, SEO or Data wanted something that conflicted with an engineering constraint. How did you size the impact and reach a decision?
- Describe an abstraction or shared component you built that other teams adopted. How did you know it saved them work?
- Describe a technical decision you reversed. What changed your mind and what did the reversal cost?
Reverse your own decision and price the reversal
Medium · reversibility · measurement · migrations
Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.
Approach
- State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
- Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
- Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
- Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.
Follow-up
- What in that decision was irreversible, and did you know it was irreversible when you made it?
- How did you tell the people who had already built on top of the original decision?
Tell callers you do not own that their integration breaks
Medium · deprecation · compatibility · stakeholders
A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.
Approach
- Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
- Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
- Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
- Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.
Follow-up
- How would you detect a consumer that reads the field only during a monthly export?
- One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?
Turn a code review disagreement into a decision
Easy · code review · optimistic concurrency · communication
A colleague's change updates a row with UPDATE resource SET version = version + 1 WHERE resource_id = $1 AND version = $2 and treats an affected-row count of zero as a successful no-op. You read that as a silently lost update; they think returning 200 is friendlier to clients than returning a conflict. Describe how you have handled a review disagreement of this shape: what goes in the comment, when you leave the thread, and who decides. Then write the comment you would leave here, in under 80 words.
Approach
- Sort the disagreement before writing anything. A silently discarded write is a correctness claim about data; the choice between 409 and 412 is taste. Only the first justifies blocking a merge, and saying which one you are doing is most of the value of the comment.
- Make the claim reproducible in the comment itself with an interleaving rather than a principle: A reads version 7, B reads version 7, B commits version 8, A's predicate matches zero rows, A is told it succeeded and A's edit is gone.
- Offer the alternative with its cost attached: return 409 carrying the current version and the revision that won, so the client can re-read and re-apply. Note that automatic retry is not the fix, because a retry re-reads the winner's state and reapplies an intent formed against data that no longer exists.
- Apply an escalation rule you can state: two round trips on the thread, then a call, and the service's owner decides rather than the reviewer. A reviewer who cannot be overruled is a bottleneck with extra steps.
Follow-up
- Where would you put the test that fails if someone reintroduces the swallowed zero rowcount?
- The author says clients cannot handle a 409. How do you check whether that is true?