cswork / libraryOpen practice workspace

Interview guides · Aurora Energy Research

Aurora Energy Research Software Engineer Interview Guide

Aurora Energy Research Software Engineer interview guide: two reported rounds, React, TypeScript and JavaScript questions, and a 7-day plan.

The 60-second brief

Candidates describe Aurora Energy Research as a company that builds energy research products for its clients. The Software Engineer role is reported as building applications that help users analyse energy markets and make decisions from the data, working beside product managers, data scientists and UX designers. The technologies candidates mention are JavaScript, TypeScript, Python, React, Node.js and AWS, with data visualisation tools and database systems listed as a bonus. The reported questions lean toward the browser side of that work, plus one system design question about real-time energy consumption data and one about API security.

This guide is for engineers with roughly two to four years of experience, the level candidates describe for this role, who are preparing for the Aurora Energy Research Software Engineer interview. It covers the two reported rounds, all fifteen reported questions grouped by type (JavaScript and React fundamentals, API handling in the UI, algorithms and caching, system design and security, and behavioral), and a seven-day plan.

Candidates report two rounds over about 2-4 weeks: an online screening, then an in-person interview with deeper technical questions, practical exercises and discussion of working style.

Focus areas

  • React
  • State management & React lifecycle
  • API integration (frontend)

Overview

The reported question mix is weighted toward front-end engineering. The topics candidates name are React, state management and the component lifecycle, handling API calls in the front end, TypeScript and JavaScript. Expect to explain promises, compare TypeScript with JavaScript, say where state should live in a React app, and describe what you would do about a slow interface. Prepare each as a short spoken explanation backed by a code example you can write from memory.

Beyond the front end, the reported list includes an algorithm-improvement question for a data processing application, an array transformation function, an API response caching question, a design question about real-time processing of energy consumption data, and a question on API security for a data-intensive application. Those five reward a habit of stating input size, access pattern and failure behaviour before naming a tool. The role as candidates describe it analyses large energy datasets, so think in terms of volume, freshness and what the user sees while data loads.

The behavioral side is explicit in the reports: working with a UX designer, prioritising under deadlines, responding to feedback, handling pushback on a project, and a hard technical problem you solved. The in-person round is described as including discussion of working style, and the requirements mention cross-functional teamwork and code reviews. Build five or six real stories with numbers and reuse them across prompts.

Candidates list Python and Node.js as languages the role touches and AWS as cloud experience, so be ready to discuss a small service you built or deployed, even if the questions you meet stay in the browser. Domain knowledge of energy is described as nice to have, not required, so spend your time on engineering fundamentals instead of studying the sector.

The Aurora Energy Research interview process

Candidates report two stages, an online screening followed by an in-person interview, spread over about 2-4 weeks. Treat this as candidate experience, since no official process has been published.

Online Screening

Candidates describe the online screening as a first check of your technical knowledge together with your own experience. The topics they name for the process as a whole are JavaScript, TypeScript, React, state management and API handling in the front end, so it makes sense to be fluent in those fundamentals before this stage. Prepare to define each concept plainly, show a small code example, and tie it to something you shipped.

What to demonstrate

  • Command of JavaScript and TypeScript fundamentals, such as promises, asynchronous error handling and the type system
  • Working knowledge of React state and component lifecycle behaviour
  • Ability to talk specifically about your own projects and what you personally built

How to prepare

  • Write a promise-based fetch helper from memory, then rewrite it with async and await, and explain how a rejection travels through each version
  • Prepare a two-minute account of one project: the stack, your contribution, and one decision you would change
  • List five differences between TypeScript and JavaScript and give a concrete bug that the type checker catches

In-Person Interview

Candidates describe the in-person interview as deeper technical questions, practical exercises and conversation about how you work with others. Because practical exercises are reported, plan for writing working code live, not only explaining it. The technical areas on the reported list include algorithm improvement, array transformation, API response caching, real-time data processing design and API security, alongside the React and TypeScript topics.

What to demonstrate

  • Working code on a practical exercise, which a live coding format tests directly
  • Ability to reason about data-heavy design: ingestion, caching, API protection and failure behaviour
  • Collaboration and communication, since the stage is reported to include discussion of working style

How to prepare

  • Practise writing an array transformation function without autocomplete, then state its time and space complexity aloud
  • Sketch a pipeline for real-time consumption readings on one page: ingest, store, aggregate, serve, and what you cache at each point
  • Rehearse five behavioral stories on collaboration with design, prioritisation, feedback, pushback and a hard technical problem

Where candidates lose points

Answering the React state question with a list of hooks and no view on where state belongs

Organise the answer by placement: local state for one component, lifted state for siblings, context or a store for app-wide values, and a data-fetching cache for server data. Explain effect dependencies and cleanup when a component unmounts, then give one bug caused by getting each wrong.

Handling API errors only in a catch block and missing HTTP failures that do not throw

State that fetch resolves on 404 and 500, so you check the status and throw a typed error. Pair it with loading, error and empty states, cancellation of stale requests, and a rule for which calls may be retried.

Tuning UI performance by guesswork, such as wrapping everything in memoisation

Begin with measurement in the browser profiler, find the slow commit or large bundle, and then pick the fix: stable keys, virtualised long lists, memoising an expensive child, splitting code, debouncing input. Say what you measured before and after.

Giving a tool-name answer to the real-time energy data design and the API security question

For the design, ask about reading volume, how fresh results must be and how long data is kept, then describe ingest, storage, aggregation, serving and caching with failure behaviour. For security, cover authentication, authorisation per resource, input validation, rate limiting, transport encryption and avoiding leakage of internal data in errors.

Telling behavioral stories in 'we' language with no personal decision and no outcome

For each story, name what you personally decided, the trade-off, and a measurable result. Prepare separate stories for design collaboration, prioritisation, feedback, pushback and a hard technical problem, so one story is not stretched across all five.

Questions & practice

How would you approach improving an inefficient algorithm in a data processing application?

Medium · Problem-Solving / Case Studies

Approach

  1. Measure first. Profile the run (cProfile or py-spy for Python, the Node --prof flag or the DevTools profiler for JavaScript) on realistic data volumes to find the hot spot. Before touching anything, record a baseline time and a reference output to compare against.
  2. Look for algorithmic wins: a nested-loop lookup that is O(nm) becomes O(n+m) with a hash-map index. Sort once, not inside a loop. A rolling aggregate over a time series drops from O(nk) to O(n) with a sliding window or prefix sums. Watch for hidden quadratic patterns such as spreading an accumulator inside reduce.
  3. Fix data access: replace N+1 queries with one batched query, push filtering and GROUP BY into an indexed database, and in Python replace row-by-row loops with vectorised NumPy or pandas operations.
  4. Handle scale and memory: when data outgrows RAM, stream or chunk it. Only after the algorithmic fixes, parallelise the CPU-bound, partitionable parts with worker processes, and memoise repeated pure computations.
  5. Verify: diff the new output against the reference (with a tolerance for floating-point sums), re-run the benchmark, and report the speed-up alongside any memory cost.

Follow-up

  • The dataset no longer fits in memory. What changes?
  • How do you prove the faster version gives the same results when floating-point ordering changes the sums?
  • When is an algorithm with better big-O slower in practice?

Write a function to transform an array based on specific criteria.

Medium · Coding / Algorithms

Approach

  1. Clarify the criteria and the contract first: the input element type, whether to mutate or return a new array, whether order must be preserved, and how to treat null, undefined or duplicate entries. Then write a couple of example inputs and outputs before coding.
  2. Choose the matching primitive: filter plus map for select-and-reshape, reduce into a Map or object for grouping (Object.groupBy in newer runtimes), a Set for deduplication (it keeps insertion order), flat(depth) for nesting, and a single loop for partitioning into two arrays.
  3. Keep it one O(n) pass where possible. Avoid reduce with a spread accumulator, which copies on every step and becomes O(n^2). If sorting is involved, it costs O(n log n), and Array.prototype.sort mutates in place (stable since ES2019), so copy first or use toSorted.
  4. When asked for O(1) extra space, transform in place with two pointers, for example moving all elements that match a predicate to the end while keeping the relative order of the rest.
  5. Test the edge cases out loud: an empty array, a single element, all or no elements matching, and sparse arrays (map and forEach skip holes).

Follow-up

  • Can you do it in place with O(1) extra memory?
  • How would you type this function generically in TypeScript?
  • What changes if the input arrives as a stream too large to hold in memory?

Track a rolling failure rate per destination for circuit decisions

Easy · sliding window · ring buffer · circuit breaker

The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.

Approach

  1. Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
  2. Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
  3. State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
  4. Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.

Follow-up

  • The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
  • A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?

Hold a per-tenant active cap against concurrent creates

Hard · write skew · isolation levels · constraints

A tenant on the standard plan may hold at most 50 resources with status='active'. The create handler runs SELECT count(*) FROM resource WHERE tenant_id = $1 AND status = 'active', compares to 50, then inserts. Two creates arrive 3 ms apart on different instances and the tenant lands at 51. Name the anomaly, say whether PostgreSQL 16 READ COMMITTED or REPEATABLE READ prevents it and why, then give an implementation that holds the cap at READ COMMITTED with the exact statements. Finally, say what changes when the cap is 'at most one running export per tenant' on job_run.

Approach

  1. Name it: write skew. The two transactions read an overlapping set and write disjoint rows, so there is no row-level conflict for the engine to detect and each commit is individually legal.
  2. Rule out the levels precisely. READ COMMITTED takes a fresh snapshot per statement and takes no lock on the counted rows, so both see 49. PostgreSQL's REPEATABLE READ is snapshot isolation: it removes non-repeatable reads and phantoms within the snapshot but still admits write skew, because the anomaly is not a re-read of a changed row, it is a read of a set that a concurrent transaction invalidates. Only SERIALIZABLE closes it, by tracking the read dependency and aborting one transaction with SQLSTATE 40001 — a guarantee that exists only if the application re-runs the whole transaction from the read.
  3. Convert the set predicate into a single-row conflict: keep tenant.active_resource_count and run UPDATE tenant SET active_resource_count = active_resource_count + 1 WHERE tenant_id = $1 AND active_resource_count < 50 in the same transaction as the INSERT. Zero affected rows is the cap, returned as 409. The row lock serialises the decision at any isolation level, and contention is bounded to one tenant's row — which is also the fair-scheduling unit, unlike a global counter that would convoy every tenant behind one row.
  4. State the cost you just took on: a counter is a second source of truth that can drift, so every path that changes status must adjust it inside the same transaction, and a periodic reconciliation has to exist, with resource_revision as the authority for what the count should have been.

Follow-up

  • A resource moves from archived back to active. Which statements change, and what breaks if the counter update and the status change land in different transactions?
  • The cap becomes plan-dependent and a plan can change mid-month. Where does the number 50 live, and who reads 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

  1. 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.
  2. 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.
  3. 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.
  4. 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?

What is a promise in JavaScript, and how is it used?

Medium · Technical / Domain Questions

Approach

  1. Define it: a Promise is an object for the eventual result of an async operation. It starts pending and settles once, either fulfilled with a value or rejected with a reason, and never changes after that. The executor function runs synchronously when the Promise is constructed.
  2. Explain chaining: then, catch and finally each return a new promise. Returning a value from a then handler fulfils the next promise, throwing rejects it, and returning a promise makes the next one adopt that promise's state. A missing return inside a then breaks the chain.
  3. Place it in the event loop: then/catch callbacks run as microtasks, after the current synchronous code and before the next macrotask such as a setTimeout callback.
  4. Cover async/await as syntax over promises: await inside try/catch for errors. Sequential awaits in a loop run one after another, while Promise.all runs the requests concurrently and fulfils with results in input order.
  5. Compare the combinators: Promise.all rejects on the first rejection, allSettled never rejects and returns status objects, race settles with the first to settle, and any fulfils with the first success or rejects with an AggregateError. Promises cannot be cancelled, so for fetch I use an AbortController.

Follow-up

  • What order do these logs print in: a console.log, a setTimeout(0) and a Promise.resolve().then, written in that order?
  • Implement your own version of Promise.all, including the empty-array case.
  • How would you add a timeout to a fetch call and cancel the request when it fires?

Explain state management in React and its lifecycle.

Medium · Technical / Domain Questions

Approach

  1. Start with local state: useState or useReducer. Updates are scheduled, not applied at once, and React 18 batches them automatically, including inside promises and timeouts. When the next value depends on the previous one I use the updater form, setCount(c => c + 1). I treat state as immutable, because React compares with Object.is and a mutated object will not trigger a re-render.
  2. Walk through the lifecycle: mount, update, unmount. In class components that maps to componentDidMount, componentDidUpdate and componentWillUnmount. With hooks, useEffect runs after paint whenever its dependencies change, and its cleanup runs before the next effect and on unmount. useLayoutEffect runs after DOM mutation but before paint.
  3. Decide where state lives. Lift it to the nearest common parent when siblings share it. Use Context for values that change rarely, like theme or the signed-in user, since every consumer re-renders when the value changes. Use a store with selectors (Redux Toolkit, Zustand) for shared state that changes often, and a query cache (TanStack Query, SWR) for server data instead of copying fetched data into useState.
  4. Compute derived values during render instead of syncing them through an effect, and memoise them only when they are expensive.
  5. Name the traps: stale closures in intervals and effects, missing dependencies, fetch races where an older response overwrites a newer one (fixed with an ignore flag or AbortController in the cleanup), and StrictMode's development-only mount, unmount, remount cycle, which exposes effects that never clean up.

Follow-up

  • Why does a console.log straight after setState still show the old value?
  • When would you choose useReducer over several useState calls?
  • A Context value change re-renders half the app. How do you narrow it?

What are the differences between TypeScript and JavaScript?

Medium · Technical / Domain Questions

Approach

  1. TypeScript is a superset of JavaScript that adds a static type system. tsc compiles it to JavaScript, or a tool like esbuild, swc or Babel strips the types. The types are erased, so nothing is checked at runtime, and API responses still need runtime validation, for example with a schema library such as zod.
  2. Typing is structural: compatibility depends on shape, not on declared names. Then cover inference, union types with narrowing (typeof, in, discriminated unions with a never-based exhaustive check) and generics for reusable functions and components.
  3. Compare any and unknown: any turns checking off, while unknown forces narrowing before use. Mention the strict flags such as strictNullChecks and noImplicitAny, which catch most null and implicit-any bugs.
  4. Contrast interface and type: interfaces support declaration merging and extends, while type aliases can express unions, mapped and conditional types. Most type syntax is erased, but enums, namespaces and constructor parameter properties emit JavaScript.
  5. Give the trade-off: a build step, @types packages for untyped libraries and some learning cost, in exchange for compile-time errors, safer refactors and better editor support. Adoption can be gradual with allowJs and checkJs.

Follow-up

  • How would you type a JSON response from an API you do not control without lying to the compiler?
  • What is the never type for, and where have you used it?
  • When would you pick an interface over a type alias?

How do you optimize performance in a user interface?

Medium · Technical / Domain Questions

Approach

  1. Measure before changing anything. Use the DevTools Performance panel and Lighthouse on a production build, the React DevTools Profiler to find components that re-render, and the Core Web Vitals: LCP for loading, INP for responsiveness (it replaced FID in 2024) and CLS for layout shift.
  2. Cut unnecessary renders: keep state close to where it is used, use React.memo with stable props from useCallback and useMemo, and give list items stable keys. Virtualise long lists and tables (react-window or similar) so the DOM holds only the visible rows, not all n.
  3. Reduce what reaches the client. Paginate or aggregate on the server instead of shipping raw rows, debounce requests driven by typing, cache responses, and split the bundle with React.lazy and dynamic import(). Size and lazy-load images.
  4. Keep the main thread free: move heavy computation into a Web Worker, split long tasks, and use useTransition or useDeferredValue so input stays responsive during expensive updates. For charts with many points, downsample or render to canvas instead of thousands of SVG nodes.
  5. Change one thing at a time, re-measure on the same scenario, and report the before and after numbers.

Follow-up

  • A table with tens of thousands of rows stutters when scrolling. Walk through your fix.
  • When does useMemo or React.memo make things slower?
  • How do you find out which component re-rendered and why?

Describe a challenging technical problem you faced and how you resolved it.

Medium · Problem-Solving / Case Studies

Approach

  1. Pick a problem with a real technical root cause that you drove yourself, such as a memory leak, a race condition, a slow query or a broken build. Open with the symptom and its impact in a sentence or two.
  2. Make the debugging sequence the core of the story: how I reproduced it, how I narrowed it down (logs, profiler, git bisect, a minimal repro), which hypotheses I ruled out and what evidence ruled them out.
  3. Explain the fix and why you chose it over the alternatives, including what it cost (complexity, performance, delay).
  4. Close with verification and prevention: the test, metric or alert you added, and a measured result where you have one, such as latency before and after.
  5. The answer falls apart if it stays vague ("it was tricky, and the team fixed it") or says "we" throughout, so it is unclear what you did yourself. Say "I" for your own actions.

Follow-up

  • How did you know you had fixed the root cause and not just a symptom?
  • Which approach did you try first that did not work?
  • What would you do differently if the same problem came up again?

Provide an example of how you handled project pushbacks while working with a team.

Medium · Problem-Solving / Case Studies

Approach

  1. Choose a specific instance of pushback: a stakeholder rejecting a technical approach, a scope cut, or a deadline you thought was unsafe. Say who pushed back and what was at stake.
  2. Show that you understood the objection before arguing: what the other person was worried about, and which part of it was valid.
  3. Describe how you settled it with evidence rather than persistence: a quick prototype, an estimate broken into parts, data on risk or cost, or a trade-off laid out as options with consequences.
  4. Give the outcome and your part in it, including any compromise, and what happened to the project and the working relationship afterwards.
  5. The answer falls apart if you simply gave in with no reasoning, overrode the team, or tell it so the other person comes across as the obstacle. Use "I" for your own actions.

Follow-up

  • What would you have done if the decision had gone against you?
  • Looking back, was the other person right about anything?
  • How do you raise a concern early so it does not become pushback late?

Explain how you would implement a caching mechanism for an API response.

Medium · Coding / Algorithms

Approach

  1. Scope it first: client side, server side or both; whether the data is shared or per user; how stale it may be; and what invalidates it. Only GET and other idempotent reads are safe to cache.
  2. Use HTTP caching where it fits: Cache-Control max-age, s-maxage for shared caches and CDNs, private for per-user data, ETag with If-None-Match so unchanged data returns a 304, and stale-while-revalidate to serve the cached copy while refreshing it.
  3. On the server, use cache-aside with Redis: read the key, on a miss fetch from the source and set it with a TTL, and delete or version the key when the underlying data is written. Build keys from a normalised request (method, path, sorted params), plus the user or tenant for authorised data.
  4. On the client, a bounded in-memory LRU gives O(1) get and set: a JavaScript Map keeps insertion order, so re-insert a key on access and evict the first key when full. Libraries such as TanStack Query or SWR add deduplication and background revalidation.
  5. Handle the failure cases: prevent stampedes by storing the in-flight promise so concurrent misses share one request, add jitter to TTLs, do not cache errors (or cache them only briefly), and never let a shared key serve one user's data to another. Track the hit rate and staleness.

Follow-up

  • The source data changes. How does the cache find out?
  • Implement an LRU cache with O(1) get and put.
  • Fifty requests miss the same key at the same moment. What happens, and how do you prevent it?

How would you design a system to handle real-time data processing for energy consumption analysis?

Medium · System Design / Architecture

Approach

  1. Pin down requirements before drawing anything: data sources (meters, sensors, files), event rate, latency target, consumers (live dashboard, alerts, historical analysis), and how late or out of order readings can arrive. Ask for these numbers instead of assuming them.
  2. Ingest through an API or gateway into a durable log such as Kafka or Kinesis, partitioned by meter or site ID so each source's readings stay in order. Validate the schema on entry and send bad records to a dead-letter queue.
  3. Process with a stream engine (Flink, Kafka Streams or Spark Structured Streaming) that computes tumbling or sliding window aggregates on event time, using watermarks to decide how long to wait for late data.
  4. Store raw events in object storage such as S3 for replay and reprocessing, and aggregates in a time-series or columnar store with downsampling and retention rules. Push live values to the UI over WebSockets or SSE.
  5. Address correctness and operations: delivery is at least once, so writes should be idempotent (dedupe on meter ID plus timestamp), or use checkpointing with transactional sinks for exactly-once. Monitor consumer lag, handle backpressure, and watch for hot partitions from very large sites.

Follow-up

  • A batch of readings arrives hours late. How do the aggregates stay correct?
  • A bug corrupted a week of aggregates. How do you recompute them?
  • One site produces far more data than the others and its partition falls behind. What do you change?

What considerations would you take into account for API security in a data-intensive application?

Medium · System Design / Architecture

Approach

  1. Authentication: OAuth 2.0 or OIDC with short-lived access tokens, checking signature, exp, aud and iss on every request. Hash API keys for machine clients at rest and make them rotatable. Use mTLS or signed service tokens between internal services.
  2. Authorisation: check access to each object, not just each endpoint (broken object-level authorisation is first on the OWASP API Security Top 10). Scope every query to the caller's tenant, apply least privilege, and filter fields so responses do not over-expose data.
  3. Data-heavy endpoints: use schema validation and parameterised queries against injection, cap page sizes and query complexity, apply rate limits and quotas per client (for example a token bucket), and serve large exports as asynchronous jobs behind short-lived signed URLs.
  4. Transport and storage: TLS everywhere, encryption at rest, secrets in a managed store such as AWS Secrets Manager with KMS, a narrow CORS allow-list, and no tokens or personal data in logs.
  5. Detection: keep audit logs of who accessed which dataset, alert on unusual bulk reads or scraping patterns, and scan dependencies for known vulnerabilities.

Follow-up

  • A paying customer starts downloading the entire dataset through the API. How do you detect and limit it?
  • How do you revoke a JWT before it expires?
  • How would you secure an endpoint that returns a multi-gigabyte export?

Listing latency scales with page size, not with filters

Easy · n+1 · latency · query batching

The tenant listing endpoint reads resource filtered by tenant_id and status, ordered by updated_at DESC, and returns each row plus the owner's display name from app_user and the actor of that resource's latest resource_revision. p99 is 55 ms at 10 rows per page and 1.4 s at 200. Database telemetry shows 401 statements per request, each under 1 ms, and nothing in the slow-query log. Diagnose the cause and give the fix, stating the statement count per request and the p99 you expect afterwards.

Approach

  1. Read the counters before forming a theory. 401 statements for 200 rows is one driver query plus two per row, and sub-millisecond execution with an empty slow-query log rules out a bad plan. The time is round trips, which is why it is invisible in every per-query metric and scales with rows returned rather than with filter selectivity.
  2. Name the two per-row statements from their normalised text: a single-row app_user lookup by user_id, and a resource_revision lookup by resource_id ordered by version DESC LIMIT 1. Confirm by dropping those two response fields and watching the statement count fall to one. That locates the calls in the serialisation layer, not the repository.
  3. Check that the arithmetic accounts for the whole gap. Measure one round trip to the replica in isolation; 400 trips at roughly 3 ms of network plus 0.2 ms of execution is about 1.3 s on top of a 55 ms baseline, which matches. If the multiplication had fallen short, the N+1 would only be part of the story and you would keep looking.
  4. Batch both lookups. Collect owner_user_ids and resource_ids from the driver query, then issue WHERE tenant_id = $1 AND user_id = ANY($2) for the users, and PostgreSQL's SELECT DISTINCT ON (resource_id) ... WHERE resource_id = ANY($2) ORDER BY resource_id, version DESC for the latest revision, which the UNIQUE (resource_id, version) index serves directly. On an engine without DISTINCT ON, use a lateral join or a row_number window. Three statements per request at any page size.

Follow-up

  • The page size is capped at 200 today. What breaks first if it is raised to 2,000, and is it still this bug?
  • How do you stop the next N+1 from reaching production, given that no individual query is slow and the endpoint's tests pass?

Suggested practice checklist

The plan follows the reported topic mix: JavaScript and TypeScript first, then React and UI performance, API handling and caching, system design and security, algorithms, and finally behavioral stories said out loud.

Day 1 — Map the loop and audit your stack

  • Write the two reported stages, the online screening and the in-person interview, and note what each stage covers.
  • Rate yourself from 1 to 5 on JavaScript, TypeScript, React, Node.js, Python and AWS, the technologies candidates mention for this role.
  • Pick the three weakest areas and assign each to a day below.

Deliverable: A one-page loop map plus a rated skills list with three priority gaps named.

Day 2 — JavaScript promises and TypeScript

  • Write a promise-returning function from memory, then convert it to async and await. Show how Promise.all, Promise.allSettled and Promise.race behave when one input rejects.
  • Write an unhandled rejection example and the fix, and explain the microtask queue ordering relative to setTimeout.
  • List the differences between TypeScript and JavaScript (static types erased at compile time, interfaces, generics, union narrowing, unknown versus any, strict mode) and write a snippet where the compiler catches a bug.

Deliverable: Working promise snippets and a one-page TypeScript versus JavaScript sheet with one compiler-caught bug.

Day 3 — React state, lifecycle and UI performance

  • Build a small component with useState and useReducer, lift state to a parent, and then move it to context. Note the re-render cost of each.
  • Write an effect with a dependency array and a cleanup function, and reproduce a stale-closure bug and the fix.
  • Take a long list with a slow filter and speed it up using keys, memoisation, virtualisation and code splitting. Record the profiler result before and after each change.

Deliverable: A small React sandbox with the state-placement variants and a before/after profiler note.

Day 4 — Calling APIs from the UI and caching responses

  • Write a fetch wrapper that checks response.ok, throws a typed error, supports AbortController, and retries only idempotent calls with backoff and jitter.
  • Render loading, error, empty and success states for it in a React component, with a Try again button.
  • Implement a response cache keyed by URL with a time-to-live, in-flight request sharing, and stale-while-revalidate. Note when ETag headers and invalidation after a write apply.
  • Practise the spoken version of the UI error-handling answer.

Deliverable: A fetch wrapper with an in-memory cache, a component showing all four states, and a spoken outline of the error-handling answer.

Day 5 — Data-heavy design and API security

  • Sketch the real-time energy consumption design on one page: ingest, buffering, storage, aggregation windows, serving and caching, with a note on late or duplicate readings.
  • Write a checklist for API security: authentication, per-resource authorisation, input validation, rate limiting, transport encryption, secrets handling and safe error messages.

Deliverable: A one-page design diagram, an API security checklist and a short note on pagination for large result sets.

Day 6 — Algorithms and practical coding

  • Write the array transformation function in three variants (map and filter, reduce, in-place) and state the complexity of each. Cover empty input, duplicates and non-mutation.
  • Take a slow nested-loop data processing routine, profile it, replace the repeated lookups with a Map or Set, and note the new complexity.
  • Say the improvement story aloud as problem, measurement, change and result.

Deliverable: Three working array transforms with complexity notes, one before/after optimisation, and a spoken improvement story.

Day 7 — Behavioral stories said out loud

  • Write five stories as situation, your decision, result and what you would change: a hard technical problem, pushback on a project, working with a UX designer, prioritising under deadlines, and feedback.
  • Say each story out loud and cut anything that does not show your own decision.
  • Add a number or a checkable outcome to every story and replace each 'we' with the part you did.

Deliverable: Five rehearsed stories, each with a personal decision and one measurable result.

Behavioral stories for a cross-functional engineering role

Candidates report behavioral prompts on collaborating with a UX designer, prioritising under deadlines, responding to feedback, handling pushback and solving a hard technical problem. The role is described as cross-functional with code reviews, so prepare stories about working with other disciplines, not only solo technical wins.

  1. Provide an example of how you handled project pushbacks while working with a team.
  2. Describe a challenging technical problem you faced and how you resolved it.

How do you handle errors from API calls in a user interface?

Medium · Technical / Domain Questions

Approach

  1. Start by sorting failures by kind, because each needs a different response. A network failure or timeout is retryable. A 401 sends the user back through sign-in. A 403 means they lack permission. A 404 means the resource is gone. A 422 maps server validation messages onto form fields. A 429 means back off, honouring Retry-After if present. A 5xx is retried a limited number of times and then surfaced.
  2. Remember that fetch only rejects on network failure, so check response.ok (or the status) and throw a typed error; with axios-style clients the rejection already covers non-2xx. Model each request in the component as an explicit state: idle, loading, success, error, and show an empty state separately from an error state.
  3. Retry only idempotent requests (GET, PUT) automatically, using exponential backoff with jitter and a hard attempt cap. Do not auto-retry a POST unless it carries an idempotency key, and always offer a manual Try again button. Cancel in-flight requests with AbortController on unmount or when the inputs change, so a slow stale response cannot overwrite a newer one.
  4. Keep the user's context: preserve typed form input, keep the last good data on screen with an inline warning when a refresh fails, and fail per panel so one broken widget does not blank the whole page. Show a plain-language message, never the raw server text, and log the request id and status to monitoring. React error boundaries catch render errors only, not rejected promises or event handler errors, so say you use both.

Follow-up

  • Several requests fail with 401 at once because the token expired. How do you refresh it once and replay them?
  • How would you test the error paths without a real backend?
  • The same request is retried while the first one is still in flight and the user double-clicked submit. What prevents a duplicate write?

Can you describe a time when you had to collaborate with a UX designer? What was your approach?

Medium · Behavioral / Leadership Questions

Approach

  1. Choose a project where the designer's input changed what you built, and describe your approach as steps you took: joining before the design was final, asking what user problem each screen solved, and agreeing on shared vocabulary such as component names, spacing and states.
  2. Show the engineering contribution beyond implementing mockups. Examples include raising feasibility early (for instance a dense data table that would be slow to render, with a virtualised alternative), asking for loading, empty and error states, testing the design with realistic long labels and large datasets, and agreeing on a shared component library so the two of you did not diverge.
  3. Include one real disagreement and how it was settled with evidence, such as a quick prototype, a performance measurement or feedback from a user, not by seniority. Say what each side gave up.
  4. Close with the result in something checkable (shipped on time, fewer rework cycles, a usability issue found before release) and one thing you would change. The story that sinks this answer is one where the designer is cast as an obstacle, or where you only received specs and built them.

Follow-up

  • What did you do when the design could not be built in the time available?
  • How did you handle a change from design after development had started?
  • How do you check that what you shipped matches the design, including accessibility?

How do you prioritize tasks and manage deadlines in a fast-paced environment?

Medium · Behavioral / Leadership Questions

Approach

  1. Open with the method, then spend most of the answer on a specific instance. Method: list the work, note for each item its deadline, who is waiting on it and what breaks if it slips, and rank by impact against effort. Re-rank when something new arrives instead of absorbing it silently.
  2. Use a concrete conflict from your own work, such as two urgent requests from different stakeholders. Say who you asked to decide the order (often the product manager), what you moved, and what you explicitly dropped or cut in scope. Cutting scope while keeping quality is a stronger story than working longer hours.
  3. Show communication: you flagged the risk before the deadline, offered options (smaller release now, the rest later) and confirmed the choice in writing. Mention protecting small recurring commitments such as code reviews so they do not pile up.
  4. Give the outcome with specifics (what shipped, when, what was deferred and what happened to it later). A weak version is a generic claim to be organised or a list of tools with no decision in it.

Follow-up

  • Which task did you say no to, and how did you tell the requester?
  • Two stakeholders both insist their request is the top priority. What do you do?
  • Your estimate turned out to be wrong halfway through. How did you handle it?

Tell us about a time when you received feedback. How did you respond?

Medium · Behavioral / Leadership Questions

Approach

  1. Pick feedback that was specific and a little uncomfortable, such as a code review pointing to unclear naming and missing tests, or feedback on how you communicated status. Avoid disguised compliments like working too hard, which show nothing.
  2. Describe your first reaction honestly and what you did with it: you asked a clarifying question, got an example, and checked whether the feedback was fair before deciding what to change. If you partly disagreed, say how you discussed it respectfully and what you took from it.
  3. Show the behaviour change: a concrete habit you adopted (a test checklist before opening a pull request, a short written update each day), not a vague intention to improve.
  4. Prove it worked with later evidence, such as fewer review comments of that type or a follow-up from the person who gave the feedback. Note that you now give feedback the same way. The answer fails when it is defensive or has no change afterward.

Follow-up

  • Tell me about feedback you disagreed with and what you did.
  • How do you give feedback to a peer in a code review?
  • How do you know the change stuck?