cswork / libraryOpen practice workspace

Interview guides · BCNC Group

BCNC Group Software Engineer Interview Guide

BCNC Group Software Engineer interview guide: reported code quality, refactoring and retail traffic questions, a take-home checklist and a 7-day plan.

The 60-second brief

Candidate reports describe BCNC Group as a consultancy-style employer whose engineers work on client projects, most visibly for retail and fashion customers. The Software Engineer role covers the path of a feature from analysing functional requirements to writing production code, taking part in code reviews, and working with product owners and other engineers so the result matches the client's business goals. Reported must-haves are proficiency in the programming languages of the client's stack, Git, and a working understanding of RESTful APIs. AWS or Azure, CI/CD pipelines and earlier consultancy or agency work are listed as nice-to-haves. No specific language, framework or product is named, so the stack is something to confirm with your recruiter.

This guide is for candidates interviewing for a Software Engineer position at BCNC Group. It covers the six questions candidates report (code quality on a large project, refactoring legacy code, technical debt versus delivery, edge cases in a new feature, traffic spikes on a retail platform, and a performance bottleneck), the technical assessment with its take-home style tasks, and the communication habits candidates describe around clarifying requirements and flagging delays.

Candidates describe a recruiter screening, then a technical assessment, then final evaluations, with no per-stage format reported. Take-home coding, changing scope and requirement clarity are the topics candidates tie to the assessment.

Focus areas

  • Take-home coding assignments
  • Change of scope / moving goalposts
  • Requirement clarity & specification quality

Overview

The reported questions split into two kinds. Four sit under code quality and engineering judgment: how you keep quality up on a large project, how you refactor legacy code without causing regressions, how you trade technical debt against delivery, and how you handle edge cases in a new feature. Two are case-style: architecting for traffic spikes on a retail platform and finding and fixing a performance bottleneck. Three are best answered as stories with a concrete before and after (technical debt, edge cases, bottleneck); the other three are best answered as a method you can apply to any codebase.

The technical assessment is the part candidates describe in the most detail. Reported tasks include integrating a third-party API while keeping data consistent and refactoring a supplied snippet for readability and performance. Candidates also report that clean structure, error handling and a README explaining how to run the code and why you made your design choices matter. Treat it as a small client deliverable: read the requirements line by line, write down every assumption, and submit something another engineer could pick up cold.

Reported topics also include change of scope, requirement clarity, code review feedback loops and the quality of feedback. That points at day-to-day consultancy work, where requirements shift and reviewers include client representatives. Prepare at least one story where a requirement changed mid-build and one where you raised a delay early, and prepare to say what you would ask before writing any code.

Because the languages are those of the client's stack and no single language is named, practise in the language you are strongest in and confirm with your recruiter which languages and whether a take-home is part of your loop. Refresh Git workflow, REST design (resources, status codes, idempotency, pagination, error bodies) and, if you have it, the basics of AWS or Azure and CI/CD pipelines.

Where to focus your preparation

Candidates describe a recruiter screening, a technical assessment and final evaluations. Only one preparation stage is listed below because no per-stage question assignment is reported.

Preparation focus

Candidates report that the process starts with a recruiter screening, continues with a technical assessment, and ends with final evaluations. The assessment is described as a test of following specific requirements, and candidates tie take-home style tasks, changing scope and requirement clarity to it. Which of the six reported questions come up in which stage is not reported, so prepare all six: the method questions (code quality, refactoring, edge cases, traffic spikes) as short structured answers, and the story questions (technical debt, bottleneck) in the Situation, Task, Action, Result shape. Ask your recruiter for the stage sequence and whether you will write code live or at home.

What to demonstrate

  • Working, readable code that meets the stated requirements, including error handling and edge cases
  • Whether ambiguous requirements are clarified early and delays are reported promptly
  • Whether you can explain the reasons behind your design choices
  • Engineering judgment on refactoring, technical debt and performance, shown with concrete past examples

How to prepare

  • Build a requirements checklist habit: copy every requirement from a task into a list, tick each one off against your code, and note any assumption you made
  • Write a small API integration (a third-party-style HTTP client with timeouts, retries, and idempotent writes) and a README that explains how to run it and why you built it that way
  • Rehearse spoken answers on code quality, refactoring, traffic spikes and edge cases, then two Situation, Task, Action, Result stories for technical debt and a bottleneck
  • Ask the recruiter which languages the client stack uses, whether a take-home is included, and how to submit

Where candidates lose points

Missing a small, specific requirement in the technical assessment, such as an output format, a file name or a rule about the third-party API response

Copy every requirement into a checklist before writing code and tick each one against your final submission. Re-read the original task text once more before you submit.

Guessing at an ambiguous requirement instead of asking, then delivering a working solution to the wrong problem

Write your clarifying questions down as soon as you read the task and send them early. Record every remaining assumption in the README so a reviewer sees what you decided and why.

Answering the refactoring question with 'I would rewrite it' or a list of clean-code slogans with no safety net

Name the sequence: characterisation tests around current behaviour, small reversible steps, one structural change per commit, review after each step, and a way to roll back or switch gradually.

Answering the traffic-spike question with 'add servers and a cache' without saying what breaks first

Separate read and write paths, name the first bottleneck (database connections, a slow dependency, hot stock rows), then give the relief mechanism for it, such as caching, queueing, rate limiting or degraded modes.

Telling technical debt, edge-case or bottleneck stories without numbers, your own decisions or a result

Choose stories where you made the call, record the before and after measurement, and state what you would change. Prepare each story in Situation, Task, Action, Result order and keep the situation to two sentences.

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?

Explain why the owner filter ignores the listing index

Medium · composite index · query plans · sort elimination

The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.

Approach

  1. Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
  2. For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
  3. Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
  4. Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.

Follow-up

  • 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
  • A dashboard runs this for 40 owners in one page load. What changes about the design?

Keep soft-deleted accounts from blocking re-registration

Easy · soft delete · unique constraints · engine differences

app_user holds user_id, tenant_id, email CITEXT, password_hash (NULL for SSO principals), email_verified_at, auth_version, status ('invited','active','suspended','deactivated'), created_at, updated_at, deleted_at. Two live accounts for one address inside a tenant must be impossible, but an address freed by a soft delete must be reusable, and the same tenant may delete and re-register it repeatedly. Write the uniqueness DDL for PostgreSQL 16, then the equivalent for MySQL 8 where partial indexes do not exist, and say what each permits once three deleted rows already hold that address.

Approach

  1. Start from what is actually unique: not (tenant_id, email), but (tenant_id, email) among live rows. PostgreSQL says that directly — CREATE UNIQUE INDEX app_user_live_email ON app_user (tenant_id, email) WHERE deleted_at IS NULL. A full constraint over the same two columns burns the address permanently the first time someone deletes an account.
  2. Keep case-insensitivity in the type or the index, never in the application: CITEXT as given, or UNIQUE (tenant_id, lower(email)) as an expression index where the extension is unavailable. A case-sensitive unique column is exactly how two accounts for one human appear.
  3. For MySQL 8 the predicate has to move inside the key: add a discriminator column that is a constant 0 while the row is live and is set to user_id on delete, with UNIQUE (tenant_id, email, deleted_marker). Live rows share the constant and still collide; deleted rows differ from each other and stop colliding.
  4. State the NULL variant and its dependency: leaving the marker NULL for deleted rows also works, because a unique index treats NULLs as distinct — true in MySQL, and true in PostgreSQL only under the default NULLS DISTINCT, which PostgreSQL 15 lets you reverse. Check the polarity against the three existing deleted rows: constant-on-live is what preserves the collision you want, and reversing it silently admits duplicate live accounts.

Follow-up

  • A deleted account re-registers with the same address the next day. Do the old resource rows follow the new user_id, and how does the API keep the two principals apart?
  • How do you honour an erasure request while resource_revision.actor_user_id still references this table?

Explain your process for ensuring code quality in a large-scale project.

Medium · Technical Competency and Code Quality

Approach

  1. Describe quality as layers that each catch a different class of defect, not as one practice: automated gates in CI, tests, review, and feedback from production.
  2. CI gates on every pull request: formatter and linter, type checks where the language has them, the unit test suite, and branch protection so nothing merges to the main branch without a green build and at least one approving review.
  3. Testing shape: many fast unit tests, fewer integration tests against real dependencies (a containerised database, a stubbed third-party API), and a thin end-to-end layer. Contract tests pin REST API shapes between services. Use coverage on changed lines as a signal, never as a target.
  4. Review practice: small pull requests, a description that says why the change exists, and a checklist covering error handling, edge cases, naming and tests. Record significant design decisions in short decision records and keep the README current so the next engineer can run the code.
  5. Close the loop with production data: track escaped defects, flaky tests and static-analysis findings such as complexity and duplication. Give one concrete rule you added after a real incident, and say what it caught afterwards.

Follow-up

  • How do you keep the CI pipeline fast as the test suite grows into thousands of tests?
  • What do you do about a flaky test that blocks merges but nobody owns?
  • How would you raise standards on a client codebase that has no tests and no review culture?

How do you approach refactoring legacy code while minimizing regression risks?

Medium · Technical Competency and Code Quality

Approach

  1. Pin current behaviour before changing anything: write characterisation tests (golden-master or approval tests) that record what the code does today, bugs included, so any difference shows up as a failing test.
  2. Find seams to make the code testable: inject the clock, database, HTTP client and configuration instead of reaching for globals, so the legacy logic runs in a test harness without its environment.
  3. Change in small behaviour-preserving steps using automated IDE refactorings (extract method, rename, move). Keep refactoring commits separate from behaviour changes so review stays readable and git bisect can isolate a regression.
  4. For larger rewrites use the strangler fig pattern: put an interface in front of the old code, move callers across one at a time, and keep the old path behind a feature flag. On critical paths, run old and new in parallel and diff their outputs before switching traffic.
  5. Prioritise by risk and value: hotspots with high change frequency (from git history) and high complexity come first; code that never changes rarely repays the effort. Define rollback (flag off, revert) and the metrics you watch, such as error rate and latency, after each release.

Follow-up

  • The module mutates shared global state and has no tests at all. What is your first concrete step?
  • How would you justify the refactoring time to a product owner who only sees feature delivery?
  • How do you refactor a database schema the legacy code depends on without downtime?

Describe a situation where you had to balance technical debt with the need for immediate delivery.

Medium · Technical Competency and Code Quality

Approach

  1. Open with a concrete constraint and a specific shortcut, not a general philosophy. For example: a fixed release date, and I shipped a synchronous call where a queue belonged, or hard-coded configuration a client later needed to vary.
  2. Show the debt was deliberate: say what I knew the shortcut would cost, how I contained it (behind an interface, covered by tests, behind a flag), and the condition that would trigger repaying it, such as traffic above a threshold or a second consumer of the data.
  3. Show the communication: I told the product owner or client what was deferred and what the risk was, agreed it explicitly, and logged the repayment work as a ticket with an owner instead of a code comment nobody reads.
  4. Name the lines I would not cross for a deadline, such as security, data integrity and losing user data, and why those trade-offs differ from cosmetic or performance debt.
  5. Close with a measurable result: delivered on the date, then repaid within a stated window with an outcome (latency down, an incident class gone). Or, if it was never repaid, say what it cost and what I changed afterwards. A story where the debt was never acknowledged, or where someone else gets the blame, undermines the answer.

Follow-up

  • What interest did you actually pay on that debt before it was repaid?
  • How did you get the repayment work prioritised against new features?
  • Looking back, would you take the same shortcut again, and what would change your answer?

How would you architect a system to handle high traffic spikes for a retail platform?

Medium · Problem-Solving and Case Studies

Approach

  1. Start by clarifying requirements: a flash sale or a seasonal peak, peak versus baseline requests per second, and the read/write ratio (browsing far outweighs checkout). Then split strongly consistent data (inventory, orders, payments) from data that tolerates staleness (catalog, product pages, recommendations).
  2. Read path: a CDN for static assets and cacheable product pages, then a cache-aside layer (for example Redis) for catalog data with TTL jitter and request coalescing so a hot key expiring does not stampede the database. Add read replicas behind it, and connection pooling so the app tier cannot exhaust database connections.
  3. App tier: stateless services behind a load balancer with autoscaling on CPU or request rate. Autoscaling reacts with a lag, so pre-scale before a known event and keep warm capacity.
  4. Write path: protect inventory with an atomic conditional update (decrement only while stock is above zero) or a short-lived reservation with expiry, so the last unit is never oversold. Put order processing behind a queue to absorb bursts, and require idempotency keys on order creation so client retries do not double-charge.
  5. Protection and verification: rate limiting (token bucket), a waiting room for flash sales, timeouts and circuit breakers on third-party calls such as payment, and graceful degradation that switches off non-essential features first. Load test above the expected peak, and watch p99 latency, error rate and queue depth during the event.

Follow-up

  • Two customers try to check out the last unit at the same moment. Walk through exactly what happens.
  • The payment provider's response time jumps to several seconds mid-sale. How does the system behave?
  • How would you warm caches and capacity before a sale starts at a fixed time?

Every query on one table stalls for forty seconds mid-deploy

Medium · lock contention · ddl · migrations

During a release on PostgreSQL, every query touching resource times out for about 40 seconds and then recovers with no intervention. The release ran one migration, ALTER TABLE resource ADD COLUMN archived_reason TEXT, and the migration log shows it completing in 6 ms. Unrelated tables showed no change in error rate. Explain how a 6 ms statement caused a 40-second stall, give the ordered checks you would run on a live system to confirm it, and give the migration procedure that prevents a repeat.

Approach

  1. Separate the statement's duration from the lock's duration. ADD COLUMN with no default is a catalogue-only change and genuinely runs in milliseconds, but it requires ACCESS EXCLUSIVE, and it cannot acquire that until every transaction already touching the table has finished.
  2. Account for the queueing, which is the part that surprises people. A lock request that is waiting blocks later requests for conflicting modes behind it rather than letting them overtake, so one long-open transaction holds the DDL and the DDL holds all the traffic. The stall length is set by the longest open transaction, not by the size of the change.
  3. Confirm on a live system in this order: pg_stat_activity for that table ordered by xact_start, looking for the oldest transaction and specifically for state = idle in transaction; then pg_locks where granted = false to find the waiter; then join them on pid to name blocker and blocked. pg_blocking_pids() does that join for you and is the fastest single call.
  4. Prevent rather than merely time it better. Set lock_timeout to a second or two on the migration session so the DDL abandons the queue after a bounded wait and is retried, instead of holding it for as long as the oldest transaction lives. Be exact about what that buys: queries arriving during the wait still queue behind the pending ACCESS EXCLUSIVE request, so each attempt costs them up to one lock_timeout of added latency. The outage goes from 40 seconds to about one second per attempt, not to zero. Also run migrations away from deploy-time peaks, and put a statement timeout and an idle-in-transaction timeout on the analytics role that opens the long transactions.

Follow-up

  • The same release also wants NOT NULL on that column. What is the sequence that gets there without a long lock?
  • Your lock_timeout retry fails ten times in a row because the analytics transaction is always open. What do you change?

Suggested practice checklist

Each day targets a reported question, the assessment style candidates describe, or a stack topic from the requirements, and ends with something you can show: code, a README, a written answer or a story.

Day 1 — Confirm the format and refresh the stack basics

  • Write the questions to ask your recruiter: which languages the client stack uses, whether the technical assessment is a take-home, and how and where to submit.
  • Refresh REST design in your main language: resource naming, status codes, idempotency keys, pagination and error body shape.
  • Refresh your Git workflow: branching, rebasing against merging, and writing a commit history a reviewer can follow.

Deliverable: A list of recruiter questions and a one-page REST and Git cheat sheet in your own words.

Day 2 — Build a take-home style API integration

  • Write a small client for a third-party-style HTTP API with timeouts, bounded retries and idempotent writes so that retried calls do not duplicate data.
  • Add tests for the failure paths: timeout, malformed response, duplicate delivery and partial failure.

Deliverable: A small repository with the client, its tests and a README covering how to run it and why you made each design choice.

Day 3 — Refactor under a safety net

  • Take a messy 100-line function from your own work and write characterisation tests that pin its current behaviour first.
  • Refactor in small steps, commit each, and write the commit messages so a reviewer can follow the reasoning.
  • Practise answering aloud how you refactor legacy code while minimising regression risk, and what you would do about code with no tests at all.

Deliverable: A before-and-after diff with passing characterisation tests, plus a spoken answer to the refactoring question.

Day 4 — Code quality and technical debt answers

  • Write your process for keeping quality up on a large project: review rules, automated checks in CI, test layers, ownership and how you measure it.
  • Write a Situation, Task, Action, Result story where you chose between paying down technical debt and shipping, including who agreed and what you recorded.

Deliverable: A one-page code-quality answer and a finished technical-debt story, both rehearsed aloud.

Day 5 — Retail traffic spikes and slow queries

  • Sketch a retail platform for a sudden traffic spike: separate read and write paths, cache the catalogue, queue order writes, protect stock rows from contention, and plan degraded modes.
  • Write down what you would measure first and what breaks first at ten times the load.

Deliverable: An annotated design sketch with a bottleneck list, and a written diagnosis sequence for slow queries.

Day 6 — Edge cases, bottlenecks and estimates as stories

  • Prepare the edge-case answer: your method of listing boundaries, states and failures, with one real example turned into tests.
  • Prepare the performance-bottleneck story with a baseline measurement, the diagnosis steps, the fix and the result.

Deliverable: Two rehearsed stories in Situation, Task, Action, Result form and one written estimate with its assumptions.

Day 7 — Scope change, clarity and a full dry run

  • Write a short story about a requirement that changed mid-build, covering what you renegotiated, and another about a delay you raised early.
  • Take a deliberately vague task description and write the clarifying questions and recorded assumptions you would send.
  • Run one full mock: a timed coding task from your day 2 repository style, then two spoken answers, then write the questions you want to ask the team.

Deliverable: Two scope-and-delay stories, a clarifying-question list for a vague task and a completed mock with notes.

Stories to prepare: edge cases, technical debt, bottlenecks and changing scope

The reported behavioural questions are tied to engineering judgment: edge cases, technical debt, bottlenecks. Candidates also report topics on changing scope, requirement clarity and code review feedback, which fit client-facing project work. Choose stories where you made the decision, give the measurable result, and name what you would do differently.

  1. Describe a situation where you had to balance technical debt with the need for immediate delivery.
  2. Tell me about a time a requirement changed after you had started building. What did you do first?
  3. Tell me about a time you raised a delay or a risk early, and what happened as a result.
  4. Tell me about feedback you received in a code review that you disagreed with, and how you resolved it.

How do you handle edge cases when designing a new feature?

Medium · Technical Competency and Code Quality

Approach

  1. Treat this as a method plus one story. Start with the method: I list the inputs, states and actors for the feature, then probe each with boundaries (empty, null, maximum size, duplicates, unusual characters, time zones, rounding), state transitions (what if the step runs twice, out of order or half-finished), and failure of each dependency.
  2. Turn the list into decisions: for each edge case I say whether it is handled now, rejected with a clear error, or explicitly out of scope and recorded as an assumption. The mistake that sinks the answer is a generic list with no decisions and no evidence.
  3. Make it concrete with a retail-style example from your own experience, such as an order submitted twice, stock reaching zero between cart and checkout, or a discount applied to an already discounted line. Say how each case became an acceptance criterion or an automated test before the code was merged.
  4. Show the Result: name a case you caught early and the incident it prevented, or one you missed and what you changed afterwards. If requirements were unclear, mention the question you asked the product owner rather than guessing.

Follow-up

  • Which edge case did you decide not to handle, and who agreed to that?
  • How do you stop edge-case handling from making the feature code hard to read?
  • How did you find the cases you had not thought of, for example from production logs or review comments?

Describe a time you faced a significant performance bottleneck; how did you identify and resolve it?

Medium · Problem-Solving and Case Studies

Approach

  1. Pick a bottleneck you personally diagnosed and set it out as Situation, Task, Action, Result. Give the symptom in measurable terms (a slow endpoint, a timeout under load) and the baseline number you recorded before changing anything.
  2. Show the diagnosis sequence: reproduce the problem, narrow it with measurement (application profiler or tracing, database query plans, load-test output), form a hypothesis, then change one thing at a time. The mistake that sinks the story is guessing and optimising without a measurement.
  3. Name the fix at the right level, for example a missing index or a query that fetched too many rows, an N+1 pattern, a missing cache, a blocking call on the request path, or a payload that was too large. Say what trade-off the fix carried, such as staleness from caching or added write cost from an index.
  4. Close with proof: the same measurement after the change, how you checked that behaviour was unchanged, and what stayed behind to prevent a repeat, such as a dashboard, an alert or a regression test. Use only numbers you can stand behind.

Follow-up

  • How did you know that was the bottleneck and not a symptom of something else?
  • What did the fix cost, and what would you do if it had not been enough?
  • How would you catch the same regression earlier next time?

Estimate work you have never done and defend the range

Hard · estimation · backfills · expand-contract

You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.

Approach

  1. Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
  2. Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
  3. Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
  4. Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.

Follow-up

  • How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
  • Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?