cswork / libraryOpen practice workspace

Interview guides · Acuity Analytics

Acuity Analytics Software Engineer Interview Guide

Acuity Analytics Software Engineer interview guide: reported C# and React questions, OOP fundamentals, survey programming context and a 7-day plan.

The 60-second brief

Acuity Analytics builds software for survey programming and data analysis that it delivers to clients. Candidates describe a Software Engineer role spent writing, reviewing and deploying code for the survey programming work. That includes the Decipher ecosystem, where survey requirements are turned into interactive web experiences for clients. The stack candidates report is C# on the server and React in the browser. Candidates describe code reviews and regular work with product managers to refine requirements.

This guide is for candidates interviewing for the Software Engineer role at Acuity Analytics, including postings that carry the "Analyst - Decipher (Survey Programmer)" focus that candidates mention. It covers the five reported technical questions (React state, the C# class-versus-interface distinction, dependency injection, React performance and React lifecycle methods). It also covers the fundamentals candidates list alongside them: OOP, data structures, algorithmic complexity, memory management, C# async/await and threading, and API data fetching. Finally, it covers how to talk about survey-driven work and past projects.

Candidates report a sequence that starts with an initial technical screening and ends with a final assessment. Along the way there are technical screenings, deep dives into past projects and live coding problems. No round count or durations have been reported.

Focus areas

  • OOP Concepts
  • React
  • C#

Overview

The reported questions are all framework and language fundamentals rather than puzzles. Two are about how React holds state and runs component code over time, two are about C# design (interfaces and dependency injection), and one is about React performance. Candidates also cite two example scenarios: speeding up a slow-loading data grid in React, and tracking down a memory leak in a C# application. Together they point to a full-stack web role where you need to explain why a pattern works, not just name it.

The listed requirements are C# and React, OOP principles, and data structures and algorithms. Survey programming tools, cloud deployments and SQL are listed as nice to have. Candidates also mention memory management and algorithmic complexity as theory that gets covered, and C# async/await and threading as a topic to be ready for. So plan two kinds of practice: spoken explanations of language and framework mechanics (hooks, the class lifecycle, async/await, DI lifetimes, interface rules) and live coding in C# where you state the complexity of what you wrote.

Some postings under this title carry a Decipher survey-programmer focus. Prepare one or two examples that link your engineering work to survey-style problems: long multi-page forms, conditional skip logic, validation of answers, and moving response data from a web front end into storage and analysis. A React survey form backed by a small C# API covers most of the reported topics in one project you can talk about.

The deep-dive sessions candidates describe centre on past projects, so pick two projects you can explain end to end. For each, know the architecture, why you chose it, one bug you diagnosed and one trade-off you would revisit. The listed soft skills include explaining technical decisions to non-technical stakeholders, so practise a plain-language version of each project.

Where to focus your preparation

No fixed round order has been reported beyond a path from an initial technical screen to a final assessment. The single entry below collects what candidates report about the technical and project-discussion stages and how to prepare for them.

Preparation focus

Candidates report a process that runs from an initial technical screening to a final assessment. It mixes technical screenings, deep-dive conversations about past projects and live coding problems. The reported technical questions cover React (state management, performance, lifecycle methods) and C# (classes versus interfaces, dependency injection), with OOP and software fundamentals alongside. The reported preparation advice is to be comfortable writing basic code on a whiteboard or in a shared editor. Prepare in three layers: spoken explanations of the five reported questions, live C# coding on data-structure problems where you state the complexity, and two past projects you can walk through in depth. Ask your recruiter early whether your posting carries the Decipher survey-programmer focus, because that changes which project examples you lead with.

What to demonstrate

  • Working knowledge of C# and React as shown in the reported framework questions
  • OOP principles and software fundamentals such as data structures, complexity and memory management
  • Working code in the live coding problems candidates report
  • Your ability to explain the design choices in your past projects

How to prepare

  • Give each of the five reported React and C# questions out loud, with one code example per answer
  • Solve two data-structure problems in C# in a plain shared editor without autocomplete, and state the time and space complexity
  • Build or revisit a small React form backed by a C# API, so you have a concrete project for the state, data-fetching and DI questions
  • Write a story about diagnosing a C# memory leak and an outline of how you would speed up a slow React data grid, since candidates cite both scenarios

Where candidates lose points

Answering the lifecycle question with only class methods or only hooks, and not mapping one to the other

Learn the three phases (mount, update, unmount) plus error boundaries. For each class method, know its hook equivalent and the one place the mapping fails, such as effects running after paint or error boundaries still needing a class. Practise drawing the table from memory.

Saying interfaces cannot contain any implementation, then having no answer about abstract classes

Update your C# model: default interface methods since C# 8, static abstract members since C# 11, single class inheritance versus multiple interface implementation. Prepare one concrete pair, an interface with two implementations, that you can also reuse for the dependency injection question.

Listing React.memo and useMemo for the performance question without saying how you found the bottleneck

Open every performance answer with measurement (Profiler, Performance panel) and a diagnosis: is it load, render or data? For the slow data grid, have virtualisation, server-side paging and sorting, and deferred filter input ready, along with how you would confirm the fix.

Preparing only React and C# trivia and stalling on the live coding or fundamentals

Candidates report that data structures, algorithmic complexity and memory management are covered too. Write solutions to array, hash map, interval and graph-traversal problems in C# without IDE help, and state the Big-O after every solution. Be ready to explain the garbage collector, IDisposable and why a lingering event subscription keeps an object alive.

Having no answer when a Decipher or survey-programming posting asks how your work relates to surveys

Confirm with your recruiter whether your posting carries the survey-programmer focus. Either way, prepare one example covering form state, conditional skip logic, answer validation and getting response data to the server reliably, so you can connect your engineering work to survey data collection.

Questions & practice

Canonicalise a request body into a stable idempotency fingerprint

Medium · parsing · canonicalisation · hashing

idempotency_key.request_fingerprint is a SHA-256 over the method, path and canonicalised body, and a retry whose fingerprint differs must be rejected with 422 rather than served the stored response. Write the canonicaliser. Bodies are JSON up to 256 KB nested at most 32 levels; clients vary key order, whitespace and unicode escaping, and some send 64-bit ids as JSON numbers. Produce a deterministic byte string such that semantically identical bodies match and any semantic difference does not. State your complexity and name two normalisations you refuse to perform.

Approach

  1. Parse once into a tree, then re-serialise under fixed rules: object keys sorted, array order preserved, one escaping convention, no insignificant whitespace. Parsing is O(n) and sorting keys is O(k log k) per object, so O(n log n) overall with O(depth) stack, and the 32-level cap is enforced during parsing because hostile nesting is how a canonicaliser becomes a stack overflow.
  2. Sort keys by their UTF-8 bytes and say why the obvious implementation is wrong in some runtimes: a default string comparison that orders by UTF-16 code units places surrogate pairs, meaning code points from U+10000 up, below U+E000 to U+FFFF, which is not UTF-8 byte order, so two services written in different languages disagree on the same document.
  3. Do not re-encode numbers through a double. IEEE-754 binary64 represents integers exactly only up to 2^53, so normalising a 19-digit id through a float changes it, and 1 against 1.0 cannot be reconciled without deciding whether they are the same value. Preserve the literal token, and require ids as strings at the API boundary if you want them comparable.
  4. Reject duplicate keys rather than picking one. JSON permits them and parsers disagree, most keeping the last, so any choice you make ties the fingerprint to a parser detail that the code handling the request does not necessarily share.

Follow-up

  • A client sends the same logical request with an extra field your API ignores. Same key, different fingerprint, so you return 422. Is that the right answer?
  • Where does the fingerprint get computed relative to request decompression and the body-size limit?

Archive a resource graph without breaking live references or recursing

Medium · graph traversal · topological order · tenant isolation

Resources reference other resources within a tenant; for the largest tenant the reference table holds up to 2,000,000 nodes and 8,000,000 edges. Archiving a resource must archive everything reachable from it that nothing outside the set still references, refuse when a live external referrer exists, and terminate when references form cycles, which they legitimately do. Produce the archive order and the refusal list, targeting O(V+E). Say what stops the traversal crossing a tenant boundary, and why recursion is the wrong control structure at this size.

Approach

  1. Load the subgraph with the tenant predicate on both endpoints of the edge, not only on the side you started from. Scoping the left table alone is the classic cross-tenant leak: one mis-entered edge then pulls another tenant's resources into the traversal and, worse, into the archive.
  2. Traverse iteratively with an explicit stack. A 2,000,000-node graph can hold a chain deep enough to exhaust a native stack in the low tens of thousands of frames, and that failure is a process crash rather than an error you can return.
  3. Treat cycles as data rather than corruption: compute strongly connected components with Tarjan in O(V+E) using its own explicit stack, then condense. The condensation is a DAG, so a topological order over it gives the archive order, and every member of a component archives in one transaction because no order within a cycle is valid.
  4. Decide refusals with reverse edges. A candidate is archivable only if every in-edge originates inside the candidate set, so build the transpose or count in-degrees restricted to the visited set, and emit each blocked resource with the id of the external referrer, which is the only part of the answer an operator can act on.

Follow-up

  • The graph is read in one query and the archive writes a minute later. What can change in between, and how do you make the write safe?
  • The candidate set is 400,000 resources. Is that one transaction, and if not, what does a half-finished archive look like to a reader?

Find overlapping job attempts and peak concurrency from lease records

Medium · sweep line · intervals · leases

A day of job_run history yields about 50,000,000 attempt records: (job_run_id, job_type, attempt, started_at, finished_at which is NULL when the worker died, lease_expires_at). Leases expire on a clock, so a job that outran its lease ran twice. Produce (a) every job_run_id whose attempts overlapped in wall-clock time and (b) the peak number of simultaneously running attempts per job_type with the minute it occurred. Target O(n log n). State how you treat a NULL finished_at and what clock skew does to your answer.

Approach

  1. Define the interval before sorting anything: an attempt occupies [started_at, COALESCE(finished_at, lease_expires_at)). finished_at is observed and lease_expires_at is only a promise, so every attempt without a finish contributes an estimate and the whole result is a lower bound on overlap rather than an exact count.
  2. For peak concurrency, sweep: emit 2n endpoints, sort by (timestamp, kind) with ends ordered before starts at equal timestamps, then walk the sequence maintaining a counter per job_type and record each type's maximum with its timestamp. O(n log n) dominated by the sort, O(n) space, or O(1) extra if the sort is external and the walk streams.
  3. For overlap detection, do not compare attempts pairwise. A single global sort by (job_run_id, started_at) gives both the grouping and the order; within a group, keep the maximum end seen so far and report an overlap exactly when the next start is less than that running maximum, which is one linear pass after the sort.
  4. Half-open intervals matter and are easy to get wrong: with closed intervals an attempt ending at the same millisecond another begins reads as concurrency two, and across 50,000,000 records that artefact swamps the real signal.

Follow-up

  • A handler is not idempotent and you have found 400 overlapping jobs. Which of them actually caused damage, and what would you query to find out?
  • Peak concurrency for one job_type is 4 against a configured cap of 4. Is the cap working, or is the data hiding attempts that never started?

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

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

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

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

How do you manage component state in React?

Medium · Technical Proficiency & Frameworks

Approach

  1. Start with local state: useState for independent values, and useReducer when several fields change together or the next state depends on an action type. A multi-page survey form is a good example: one reducer holds the answers in a map keyed by question id and handles actions like answer, next and back.
  2. Don't store anything you can compute. Visibility from skip logic, completion percentage and validation errors can all be derived from the answers during render (wrapped in useMemo if expensive). Copying them into state creates values that drift out of sync.
  3. When siblings need the same data, lift it to the nearest common parent. Use Context for values that change rarely, like theme, the current user or the locale. Every consumer re-renders when the context value changes, so frequently updated data in one big context slows the tree.
  4. Keep server state separate from UI state. Fetched data belongs in a cache layer (React Query, SWR or RTK Query) that handles loading, errors and refetching. Use a store such as Redux Toolkit or Zustand only when a lot of client state is shared across distant parts of the tree.
  5. Common bugs: mutating state instead of returning a new object, using a plain setter when the next value depends on the previous one (use the functional form, setCount(c => c + 1)), and stale closures in effects that leave values out of the dependency array.

Follow-up

  • A survey has 200 questions and typing in one input re-renders the whole form. How do you fix it?
  • When would you move state out of Context into an external store?
  • How would you keep a respondent's partial answers if they reload the page?

Can you explain the difference between a class and an interface in C#?

Medium · Technical Proficiency & Frameworks

Approach

  1. A class is a concrete type. It can hold state in fields, has constructors and implementation, and can be instantiated unless it is abstract. A class inherits from only one base class. An interface is a contract of members that any class or struct can implement, and a type can implement many interfaces.
  2. Interfaces cannot have instance fields or constructors. Since C# 8 they can have default method implementations, and since C# 11 they can declare static abstract members. If you say "interfaces have no implementation", add that this is no longer strictly true.
  3. Compare with abstract classes. Use an abstract class to share code and protected state across closely related types. Use an interface for a capability that unrelated types share (IDisposable, IComparable<T>). Note the versioning cost: adding a member to an interface breaks every implementer unless you give it a default implementation.
  4. Use a concrete example, such as an IAnswerValidator interface with RangeValidator and RequiredValidator classes behind it. Callers depend on the interface, so you can add validators or mock them in tests without changing the calling code. This leads naturally into the DI question.
  5. Edge cases: casting a struct to an interface boxes it (a heap allocation), and explicit interface implementation resolves name clashes between two interfaces or hides a member from the class's public surface.

Follow-up

  • When would you pick an abstract class over an interface?
  • What problem do default interface methods solve, and what new problems do they create?
  • What happens in memory when a struct is used through an interface reference?

What are the advantages of using Dependency Injection in your code?

Medium · Technical Proficiency & Frameworks

Approach

  1. Define it first: a class gets its dependencies through its constructor, usually typed as interfaces, instead of creating them with new. Wiring happens in one composition root. This is the dependency inversion principle put into practice.
  2. Give the concrete advantages. You can unit test by passing in fakes or mocks (an IClock instead of DateTime.Now, a fake repository instead of a database). Implementations can be swapped through configuration. Coupling between modules goes down, and the container manages object lifetimes and disposal for you.
  3. Know the lifetimes, using ASP.NET Core's built-in container as the example. Transient creates a new instance per resolution, Scoped one per request, and Singleton one for the app. The classic bug is a captive dependency: a singleton holding a scoped service such as a DbContext, which outlives its scope and gets used across threads.
  4. Name the costs too. Indirection makes code harder to follow, a missing registration only fails at runtime, and wrapping every class in an interface is over-abstraction. Calling the container from inside business code is the service-locator anti-pattern, which hides dependencies.
  5. Prefer constructor injection for required dependencies, so an object cannot exist in an invalid state. Mention method or property injection only for optional ones.

Follow-up

  • What goes wrong if a singleton service depends on a scoped one, and how would you detect it?
  • How would you unit test a class that calls DateTime.Now and an external HTTP API?
  • When is introducing an interface purely for DI not worth it?

How do you optimize a React application for performance?

Medium · Technical Proficiency & Frameworks

Approach

  1. Measure before changing anything. Use the React DevTools Profiler to find components that render too often or too slowly, the browser Performance panel for long tasks, and Lighthouse or Web Vitals for load metrics. Then classify the problem: slow load (bundle size), slow interaction (renders) or slow data (network and waterfalls).
  2. Render fixes: move state down so updates touch fewer components, split contexts, use React.memo on expensive children, and use useCallback and useMemo to keep props stable. Inline object or function props defeat memo. Use stable keys, never array indexes, in lists that can reorder.
  3. For the slow data-grid scenario candidates cite: virtualise the rows with react-window or react-virtual so only visible rows mount, page or sort on the server instead of shipping every row, and debounce filter input. useDeferredValue or useTransition keeps typing responsive while the grid recomputes.
  4. Load fixes: route-level code splitting with React.lazy and Suspense, removing heavy dependencies found with a bundle analyser, tree-shaking, and correctly sized images. Data fixes: fetch in parallel instead of in waterfalls, and cache responses.
  5. Finish by checking the result. Re-profile with the same interaction and compare commit counts or interaction latency. Unnecessary memoisation adds comparison cost and complexity, so only apply it where the profile shows a gain.

Follow-up

  • The grid has 50,000 rows with sorting and filtering. Where does each operation run, and why?
  • How do you prove the change helped and did not just move the cost?
  • When does useMemo make a component slower?

Explain the lifecycle methods in React and when to use each one.

Medium · Technical Proficiency & Frameworks

Approach

  1. Mounting: the constructor initialises state and binds handlers, render stays pure, and componentDidMount starts fetches and subscriptions and measures the DOM. static getDerivedStateFromProps runs before every render and is rarely needed.
  2. Updating: shouldComponentUpdate (or PureComponent) skips renders. getSnapshotBeforeUpdate captures DOM information such as scroll position just before the commit. componentDidUpdate(prevProps, prevState) reacts to changes: compare prevProps before fetching or calling setState, because an unconditional setState, including one in a fetch callback, loops forever, and an unconditional fetch re-runs on every update.
  3. Unmounting and errors: componentWillUnmount clears timers, unsubscribes and aborts in-flight requests. getDerivedStateFromError and componentDidCatch build error boundaries, which still need a class component. componentWillMount, componentWillReceiveProps and componentWillUpdate are legacy and only remain as UNSAFE_ versions.
  4. Map them to hooks. useEffect with an empty dependency array covers mount, its cleanup covers unmount, and a dependency list covers updates when those values change. useLayoutEffect runs synchronously before paint for measurement, and React.memo plays the role of shouldComponentUpdate.
  5. State where the mapping breaks down. Effects run after paint, not at the same point as componentDidMount. React 18 Strict Mode mounts, unmounts and remounts in development to expose missing cleanup. A fetch that resolves after unmount needs an AbortController or an ignore flag.

Follow-up

  • Why does calling setState in componentDidUpdate without a condition cause an infinite loop?
  • How would you write an error boundary around a survey section that might crash?
  • Why does an effect run twice in development under Strict Mode, and what does that tell you about your cleanup?

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

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

Each day takes one of the topic areas candidates report (C# and OOP, dependency injection and async, React state and lifecycle, React performance, live coding, survey programming, project deep dives) and practises the reported questions that belong to it.

Day 1 — C# types and OOP fundamentals

  • Write and speak a short answer on class versus interface in C#, covering single inheritance, multiple interfaces, default interface methods and abstract classes
  • Code an IAnswerValidator interface with two implementations and one abstract base class, and note where each choice fits
  • Write one-sentence definitions with a C# example for encapsulation, inheritance, polymorphism and abstraction

Deliverable: A recorded class-versus-interface answer, a compiling validator example with an abstract base, and a one-page OOP definitions sheet.

Day 2 — Dependency injection, async/await and memory in C#

  • Wire the Day 1 validators into a small console app or ASP.NET Core API through constructor injection, and write one unit test that injects a fake
  • Write examples of the transient, scoped and singleton lifetimes and of a captive dependency, then explain out loud why the captive one is a bug
  • Write a C# async note with code: what await compiles to (a state machine that frees the thread during I/O), why .Result or .Wait() can deadlock under a UI or classic ASP.NET synchronization context, why async void belongs only in event handlers, Task.Run for CPU-bound work versus await for I/O, ConfigureAwait(false) in library code, and passing a CancellationToken
  • Write a STAR story about troubleshooting a C# memory leak (or a performance bug if you have no leak story): the symptom, the tool (dotMemory, Visual Studio diagnostics, dotnet-counters), the cause and the fix

Deliverable: A DI-wired project with one passing test using a fake, a lifetimes note with the captive-dependency example, an async note with a deadlock example and a cancellable call, and a written memory-leak story.

Day 3 — React state and the component lifecycle

  • Build a three-page survey form in React using useReducer, with answers keyed by question id and skip logic derived during render
  • Rewrite one component as a class and mark which lifecycle method matches each hook in your function version
  • Answer the state-management and lifecycle questions out loud, each with one example from the form

Deliverable: A working React survey form, a class-to-hooks mapping table, and two recorded spoken answers.

Day 4 — React performance and the slow data grid

  • Render 10,000 rows in a table, profile it with React DevTools, then add virtualisation and memoised rows and profile again
  • Add a debounced filter and code-split the grid route with React.lazy, then measure the bundle before and after
  • Write a five-step outline for speeding up a slow-loading data grid: measure, classify, fix render, fix data, verify

Deliverable: Before-and-after profiler screenshots, a bundle-size comparison, and a written five-step grid optimisation outline.

Day 5 — Live coding in C# with stated complexity

  • Solve the request-fingerprint canonicalisation problem in C# in a plain editor, then state its time and space complexity
  • Solve the overlapping-job-attempts problem with a sort and sweep: explain why peak concurrency takes O(n log n), and why listing every overlapping pair takes O(n log n + k) for k overlaps
  • Solve the resource-graph archive problem iteratively with an explicit stack, and list the edge cases (cycles, shared nodes)

Deliverable: Three C# solutions, each with a complexity line and a list of the edge cases you tested.

Day 6 — Survey data, the API layer and SQL

  • Add a C# endpoint that accepts survey responses from the Day 3 form using async/await, and handle validation errors and retries on the client
  • Write the update query that detects a concurrent edit, in case SQL comes up for database work
  • Write a short explanation of how your form handles skip logic, partial saves and validation, aimed at a non-technical project manager

Deliverable: A running React-to-C# submission flow, one SQL concurrent-edit query, and a plain-language explanation of the survey flow.

Day 7 — Project deep dives and a full mock

  • Prepare two past projects with architecture, a key decision, one bug you diagnosed and one trade-off you would change
  • Rehearse the behavioral prompts on explaining a decision to a non-technical stakeholder, the code-review disagreement and the ambiguous requirements, plus the memory-leak story from Day 2
  • Run a mock with a friend covering the React state-management question, the C# class-versus-interface question and one coding problem, then write three questions to ask about the team's work

Deliverable: Two project walkthrough outlines, four rehearsed stories, notes from a completed mock, and three questions for your interviewers.

Behavioural preparation

Candidates describe deep-dive sessions about past projects, and the listed soft skills are clear communication, collaboration and explaining technical decisions to non-technical stakeholders. The responsibilities candidates mention include code reviews and refining requirements with product managers. Your stories should show a technical decision, its reasoning, and how you communicated it.

  1. Describe a time you had to troubleshoot a memory leak in a C# application.
  2. Tell me about a technical decision you had to explain to a non-technical stakeholder. What did you leave out, and how did you check they understood?
  3. Describe a code review where you gave feedback a peer disagreed with, or received feedback that changed your design. What happened next?
  4. Tell me about a time requirements from a product manager or client were ambiguous. How did you turn them into something buildable?
  5. Describe a question you could not answer on the job and the steps you took to find the answer.

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

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

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

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

Unblock an engineer without taking the keyboard

Easy · mentoring · leases · at-least-once

A teammate has spent two days on a job handler that occasionally writes duplicate rows. They are certain the queue is delivering twice by mistake. You suspect a lease expiring under a slow handler, so the job is running concurrently with itself. Describe how you have unblocked someone in this position: what you asked before offering a hypothesis, what you showed them rather than told them, and what you left them owning. Then say what you would do if their theory turned out to be the right one.

Approach

  1. Ask before diagnosing, and ask for things answerable from data they already have: the attempt count on the job rows that produced duplicates, the handler's observed duration against its lease expiry, and whether the duplicate rows share a natural key that a unique constraint could have caught.
  2. Teach the shape rather than the answer. A lease cannot distinguish a dead worker from a slow one, so a handler that outruns its lease is running twice by design, and deploys deliver the other half by killing handlers mid-run on every rollout. Both of their candidate theories produce identical duplicate rows, which is why the evidence has to come from timings rather than from argument.
  3. Hand over a checklist they execute: a natural key on every write the handler performs so the second copy collides rather than appends, the record of intent written before any external effect, a lease heartbeat while running, and the metric that shows it working.
  4. Keep ownership with them deliberately. Pair on the first write, then step back; if you finish it yourself you have closed one ticket and left the same person stuck on the next redelivery.

Follow-up

  • How would you distinguish a genuine double-delivery from a lease expiry using only the data already stored?
  • Their handler calls an external endpoint before recording that it did. What do you tell them to change first?