Interview guides · Apto Solutions
Apto Solutions Software Engineer Interview Questions & Guide 2026
API design practice for Apto Solutions software engineer candidates: compatibility, versioning, and changes you cannot take back.
The 60-second brief
As a Software Engineer at Apto Solutions, you are part of the company's technical delivery, responsible for building and maintaining the systems behind its client-facing products. Whether you are working with Ruby on Rails, Angular, or specialized data quality frameworks, your contributions affect how Apto Solutions' users interact with its platforms.
Getting the code to run is the floor. What usually separates answers is the case checked without prompting: empty input, a single element, duplicate keys, or a value that overflows the integer type you chose.
Focus areas
- Diagnose failures inside environments you cannot instrument
- Version an API you cannot force clients to upgrade
- Make integration retries idempotent without target-side idempotency keys
Overview
This role is critical because it bridges the gap between complex business requirements and scalable software solutions. You will be expected to thrive in environments that demand both high-level system architectural thinking and the precision required for high-quality code. Candidates report that Apto Solutions favors engineers who are technically proficient and who contribute to a collaborative team culture where problem-solving is shared.
The Apto Solutions interview process
Apto Solutions candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Psychometric Assessment
Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What to demonstrate
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Individual Interviews
An unlabelled round is first an information problem, and the cheapest information is free. Whoever schedules it can usually tell you how long it runs, who will be in the room and what they work on, whether you will be writing code and in what environment, and whether anything is being sent beforehand. Ask in writing so the answer is on record, then prepare for the two or three formats those answers still leave open instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.
What to demonstrate
- Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
- Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
- Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating
How to prepare
- Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
- Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
- Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes
Technical Assessment
The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
Where candidates lose points
Validating against a client sandbox and assuming production parity
Sandboxes typically carry smaller data, looser or absent rate limits, a schema version behind production, and sometimes synchronous behaviour where production is asynchronous. Code that passes there fails first in the client's production, during a change window you do not control and often cannot get a second one of. The mitigations are specific: assert the observed schema version at the boundary of every run, measure the production rate limit empirically rather than reading it from a document, and design the first production run to be a bounded, reversible slice rather than a full backfill.
Tenant context leaking across a connection pool
Setting the tenant on a connection and relying on it for the rest of the request looks correct in every test that runs one request at a time. Under transaction pooling the connection goes back to the pool with that session state still attached, and the next checkout, serving a different client, inherits it; row-level security then enforces the previous tenant's policy flawlessly and returns the wrong rows. It reproduces only under concurrency, which is the load profile your integration tests do not have. Use a transaction-scoped setting, and assert that the setting matches the request's tenant immediately before the first query rather than trusting that it was set correctly upstream.
Saying 'eventually consistent' without naming the anomaly a user would see
Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.
Arguing past a hint
When the interviewer asks what happens for a particular input or floats a different data structure, stop and take it seriously; it is almost always a correction rather than idle curiosity. Talking over it converts a recoverable wrong turn into a data point about how you handle review.
Questions & practice
How would you design and construct a Battleships-style game from the ground up?
Medium · data structures and algorithms
Approach
- Choose the data structure from the access pattern, not from familiarity.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- What is the worst case, and how likely is it on real data?
- Which test case would catch an off-by-one here?
Compare release versions and report the live estate spread
Easy · parsing · version skew · ordering
Each environment row carries reported_release as TEXT: either NULL, or a version of the form MAJOR.MINOR.PATCH with an optional -rc.N suffix. Up to 400 environments. Ordering rule: compare major, then minor, then patch numerically; a -rc.N build precedes the same MAJOR.MINOR.PATCH with no suffix, and rc numbers compare numerically. Return the lowest and highest reported versions, the minor-version spread between them, and the count of environments whose reported_release is NULL. NULL means the environment's version is unknown, not that it converged.
Approach
- Parse each string once into a comparable tuple: (major, minor, patch, is_final, rc), where is_final is 1 for a plain release and 0 for a pre-release, and rc is 0 when absent. Tuple comparison then yields the stated order for free, including 3.10.0-rc.1 < 3.10.0. O(L) per string, O(n*L) total.
- Track min and max in one linear scan rather than sorting: O(n) time, O(1) extra space, and n is only a few hundred so the parse dominates either way.
- Fail loudly on a string that does not match the grammar instead of defaulting it to 0.0.0 or to the newest release. An unparseable reported_release is a reconciliation-ingest bug, and either default hides it in the direction that looks safe.
- Compute the minor spread only when min and max share a major version. Across majors the difference of minor numbers is meaningless, so return the pair of versions and flag the condition rather than a number.
- Report the NULL count as its own output line. Folding unknowns into 'converged' is how an install ends up running a release nobody knew was still live.
Follow-up
- Two majors are live at once. What does 'spread' mean now, and what single number, if any, would you still publish?
- reported_release is a claim from the environment, and last_heartbeat_at may be days old for a client-managed install. At what staleness does this figure stop being usable?
- How would you enforce the supported window when the rollout plan is built, instead of reporting a violation after it already exists?
Assign connector bindings to rollout waves and name the cycle
Medium · topological sort · cycle detection · dependency graph
Connector bindings declare ordering dependencies: an edge u -> v means v must not run in a wave earlier than one after u has succeeded. You have up to 50,000 bindings and 200,000 edges, and a misconfigured dependency may have created a cycle. Assign each binding the earliest wave number such that every predecessor sits in a strictly earlier wave, return the total wave count, and if a cycle exists return the bindings on one concrete cycle rather than a boolean. The implementation must be iterative: a dependency chain can be 50,000 deep.
Approach
- Build an adjacency list and an indegree array, then run Kahn's algorithm from every indegree-zero node. Bindings with no predecessors get wave 0. O(V+E) time, O(V+E) space.
- Relax waves as you decrement: when popping u, set wave[v] = max(wave[v], wave[u]+1) for each successor before enqueueing v at indegree zero. Because v is only enqueued after its last predecessor is popped, wave[v] is 1 + max over predecessors, which is the earliest legal wave. Total wave count is max(wave)+1, the longest path in nodes.
- If Kahn emits fewer than V nodes, the residual subgraph is exactly the nodes lying on or downstream of a cycle. Every residual node still has an in-edge inside the residual, otherwise it would have been popped, so walking backwards along in-edges cannot dead-end and must revisit a node within |residual| steps; the segment between the two visits is a concrete cycle. O(V) with a visited-position map.
- Use an explicit stack or a queue rather than recursion. Recursive DFS on a 50,000-node chain exceeds the default stack in most runtimes, and the failure looks like a crash rather than a cycle.
- At this size, recompute the whole plan whenever an edge changes. Incremental topological order maintenance costs far more code than the O(V+E) rebuild it saves.
Worked solution (25 min)
- Take seven bindings A,B,C,D,E,F,G with edges A->C, B->C, C->D, F->G, G->F. Indegrees: A0 B0 C2 D1 E0 F1 G1.
- Kahn from [A,B,E]: pop A (wave 0), relax C to wave 1 and indegree 1; pop B (wave 0), relax C to wave 1 and indegree 0, enqueue C; pop E (wave 0).
- Pop C (wave 1), relax D to wave 2 and indegree 0, enqueue D; pop D (wave 2). Five of seven nodes emitted.
- Residual is {F,G}. Walk backwards from F: F's in-edge comes from G, G's in-edge comes from F, so F repeats and the cycle is F -> G -> F.
- Verify the wave assignment against every edge before reporting.
Expected result: wave(A)=wave(B)=wave(E)=0, wave(C)=1, wave(D)=2, wave count 3. Cycle reported as the concrete node list [F, G]; F and G receive no wave.
Verify it yourself
- For every edge u -> v that was emitted, wave[u] < wave[v] strictly.
- The wave count equals the number of nodes on the longest emitted path (A -> C -> D = 3).
- Kahn emitted 5 nodes and V = 7, so the residual size 2 equals the reported cycle size.
- Removing edge G->F and rerunning emits all 7 nodes with wave(F)=0, wave(G)=1 and no cycle.
Follow-up
- Two bindings land in the same wave but hit the same client endpoint, whose rate budget is shared. How does the wave plan have to change, and what problem class does that turn into?
- One dependency holds only for overlapping watermark ranges rather than globally. Is the dependency graph still a DAG over bindings, and what is the right vertex if not?
- Report the critical path so an operator can see which chain sets the wave count, not just that it is five.
Find watermark coverage gaps with a window function
Medium · window functions · gaps and islands · event tables
Each succeeded connector_run claims a half-open watermark range [watermark_from, watermark_to). Ranges for one connector can overlap, and a re-run can be nested entirely inside an earlier range. Write the query that returns, per connector_id, every interval not covered by any succeeded run, as (gap_from, gap_to), ordered by connector then time. State the frame clause you use and why the default one is wrong, and give the index that removes the sort.
Approach
- Rule out lag(watermark_to) first and say why: ordered by start time, the immediately preceding row may end earlier than a row before it, so a nested or shorter range makes you report a gap that a wider earlier range already covers. The correct comparison is against the running maximum of every previous watermark_to, not the last one.
- Compute max(watermark_to) OVER (PARTITION BY connector_id ORDER BY watermark_from, watermark_to ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING). The ROWS frame is load-bearing: the default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which includes the current row, so the running max always covers the current row's own end and the query reports no gaps ever.
- Add watermark_to as a tiebreaker in the window ORDER BY so rows sharing a start instant are ordered deterministically under a ROWS frame, then emit a gap where the running max is non-null and watermark_from is strictly greater than it. The first row per connector has an empty frame and a NULL max; that is not a gap unless you define a floor such as the binding's creation time, which you should decide explicitly.
- Filter status = 'succeeded' in the window's input, and argue the classification: an ambiguous run may or may not have applied its range, so counting it as covered can drop data silently. Gap detection should treat ambiguous as uncovered and leave resolution to the reconciliation path.
- Offer the shorter alternative with its precondition: on PostgreSQL 14 and later, range_agg(tstzrange(watermark_from, watermark_to, '[)')) per connector builds a multirange, and the gaps are its complement against the window you care about. It handles adjacency and nesting without any frame subtlety, at the cost of requiring that version.
- Index (connector_id, watermark_from, watermark_to) WHERE status = 'succeeded'. It supplies the window's required ordering directly, so the plan is a WindowAgg over an index scan with no Sort, and the work is O(n) over that connector's succeeded runs.
Follow-up
- How do you distinguish a real gap from a period when the connector was deliberately paused?
- The connector has run for two years and you only care about the last 30 days. What does adding that predicate do to the correctness of the running maximum?
- How would you turn this query into an alert without it firing on the trailing edge, where the newest range is simply not written yet?
Generate invoice lines once under concurrent month-end runs
Hard · isolation levels · unique constraints · idempotency
Month-end generation reads approved work_record rows for one engagement and period, sums minutes, inserts one invoice_line whose generation_key is UNIQUE over (engagement_id, period_start, period_end, line_type, generator_version), then sets those work records to invoiced with locked_at and invoice_line_id. Two runs execute concurrently for the same engagement while late approvals are still committing. Working in PostgreSQL, name the anomaly at READ COMMITTED and at REPEATABLE READ, and specify the controls that make a retry a no-op rather than a second charge.
Approach
- At READ COMMITTED each statement takes a fresh snapshot, so the SELECT that computed the sum and the later UPDATE see different data: an approval committed between them is a phantom the sum missed but the UPDATE can still mark invoiced. Worse, an UPDATE that blocks on a row another transaction is changing re-evaluates its WHERE against the new row version once the lock releases, so WHERE status = 'approved' silently skips rows the other run already moved — fewer rows than you counted, and no error anywhere.
- At REPEATABLE READ, which in PostgreSQL is snapshot isolation, the sum and the update agree because the whole transaction shares one snapshot, but a concurrent update to the same row aborts you with serialization_failure, SQLSTATE 40001, which the application must catch and retry. Snapshot isolation still permits write skew across different rows; only SERIALIZABLE with SSI excludes it, at the cost of more 40001s and predicate-lock memory bounded by max_pred_locks_per_transaction.
- Make the second run collide instead of race: take a transaction-scoped advisory lock on a hash of (engagement_id, period_start, line_type) at the top of the run, so one generates while the other waits and then finds finished work. Keep the UNIQUE generation_key as the backstop, because the advisory lock is per-database-connection state that a failover or a second database node does not carry.
- Treat the unique violation as success. Catch SQLSTATE 23505, re-select the existing invoice_line by generation_key, and return it. INSERT ... ON CONFLICT DO NOTHING ... RETURNING returns zero rows on conflict, so a handler that trusts RETURNING writes a NULL invoice_line_id and reports a failure for work that in fact completed; ON CONFLICT DO UPDATE is worse, because it overwrites a line that has already been issued.
- Bound the read deterministically rather than by timing: select the work records FOR UPDATE ordered by work_record_id so both runs acquire row locks in the same order and cannot deadlock, and filter on approved_at < the run's start timestamp so a late approval is out of scope by definition and lands in the next period.
- Enforce immutability in the database, not in the service. A BEFORE UPDATE trigger raising when locked_at IS NOT NULL and a billed column changes, plus CHECK (status <> 'invoiced' OR invoice_line_id IS NOT NULL), means the constant and legitimate business pressure to edit an invoiced entry resolves into an appended credit_note row instead of a quiet update.
Worked solution (40 min)
- Open two psql sessions and interleave them by hand: A begins and inserts the invoice_line without committing; B begins and attempts the same insert.
- Observe B blocking on the unique index, commit A, and record the exact error B receives.
- Implement the handler path: catch 23505, re-select by generation_key, return that row, and confirm both sessions report the same invoice_line_id.
- Repeat at REPEATABLE READ with both sessions updating the same work_record to force 40001.
- Approve a work record after A's start timestamp and follow where it ends up.
Expected result: One invoice_line per (engagement, period, line_type, generator_version); the losing run returns the winner's row as success rather than an error; the late approval remains status approved, unlocked, and is picked up by the next period's run; the 40001 path is exercised by a retry rather than surfaced to the caller.
Verify it yourself
- Both sessions report the same invoice_line_id, and the table holds exactly one row for that generation_key.
- The late-approved work record still has status approved and locked_at NULL after both runs finish.
- An UPDATE against a work record with locked_at set is rejected by the trigger, not merely by the service layer that happened not to issue it.
- SELECT generation_key, count() FROM invoice_line GROUP BY 1 HAVING count() > 1 returns nothing — trivially, because the constraint and not the code is what guarantees it.
Follow-up
- A line has been issued and then an approval behind it is reversed. What rows do you write, and what does the original line look like afterwards?
- How many times does the run retry before giving up, and what state does it leave behind for the operator who picks it up?
- After the fact, how do you prove no engagement-period was billed twice, without trusting the application code that wrote it?
How would you explain the concept of Polymorphism in a real-world project?
Medium
Approach
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
What are the primary differences between an array and an ArrayList?
Medium
Approach
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
What is the difference between an abstract class and an interface?
Medium
Approach
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Split a field in an API twelve live versions cannot upgrade
Hard · versioning · backward-compatibility · deprecation · expand-contract
Your public API returns a work-record representation carrying a single decimal hours field. You need to move to integer minutes, add an activity field older responses do not carry, and add two values to the status enum. A dozen release versions are live at once: shared-pool tenants upgrade weekly, dedicated deployments quarterly, and client-managed installs on their own schedule, currently two quarters behind. The callers are client-written integrations you cannot see or compel. Design the evolution: what you version, the sequence, the signal that removal is safe, and what breaks a client that does nothing.
Approach
- Separate the three changes, because their compatibility properties are different. Adding
minutesbesidehoursis additive and safe for any reader that ignores unknown fields. Addingactivityis additive. Adding enum values is not: a client that switches exhaustively onstatusand throws on the default branch breaks, so whether this is safe depends entirely on whether 'unknown values map to a documented fallback' was published before those clients were written. If it was not, the addition belongs behind a new major version, not in this one. - Choose the versioning unit deliberately. A major version in the path is coarse and drags every endpoint along at once; a version header or per-representation media types are finer but multiply the test matrix by the number of live combinations. With a two-quarter tail, the cheapest correct answer is additive-only evolution inside the current major, with a new major held in reserve for changes that cannot be expressed additively — and removing
hoursis exactly one of those. - Sequence it expand, dual-write, backfill, move reads, contract. Emit both fields for the whole window with
hoursderived fromminutes, so there is one source of truth and the two cannot disagree. The mistake is storing both: some path then writes one and not the other, and a client reading the stale one produces a billing discrepancy nobody can attribute. On the write path accept either field, and reject a request carrying both with inconsistent values rather than quietly preferring one. - Respect the precision problem that motivates the change, because it also constrains the transition. A value like 7.4 hours is not exactly representable in binary floating point, so
hoursround-trips inexactly and rounding to minute granularity can differ between your renderer and a client's parser. Keep integerminutesas the value of record, derivehoursfor display, and document the rounding rule so a client reconciling the two fields arrives at your number rather than its own. - Gate removal on observed convergence rather than on a date. Record the release version and environment_id against every request and expose per-field usage counts, so you can name the environments still reading
hours. Publish a Sunset header (RFC 8594) with its Deprecation companion and an end-of-life date, then be prepared to move that date: a client-managed install two quarters behind is a fact about the estate, not a communications failure. - Write down what a client that does nothing experiences at each step, because that is the contract's real specification. Additive steps: nothing. The enum addition: a failure if it switches exhaustively, which is why the fallback rule must pre-date it. Removal: a hard break, which is why it happens only behind a major version or after the usage count for that field has been zero across every environment you can see.
Worked solution (40 min)
- Write the current and target representations side by side, labelling each delta additive, behavioural or breaking with one line of reasoning.
- Write the transitional representation showing
hoursderived fromminutes, with the rounding rule stated explicitly. - Write the write path's rule for a request carrying both fields, including the inconsistent case and which field wins if you accept both.
- Design the usage telemetry: what you record per request to attribute a field read to an environment_id and release, and what you will still not be able to see.
- Draft the deprecation dates and headers, then write the single observation that authorises removing
hours. - For each step, write one sentence describing what a client that does nothing experiences.
Expected result: Additive introduction of minutes and activity inside the current major, hours derived from minutes with a documented rounding rule, a write path accepting either and rejecting inconsistent duplicates, enum additions withheld unless an unknown-value fallback was already published, per-environment field-usage telemetry, Sunset and Deprecation headers with a movable date, and removal of hours only behind a major version or against observed zero usage.
Verify it yourself
- A client compiled against the old representation and left untouched keeps working through every step before removal.
hoursandminutescannot disagree, because only one of them is stored.- You can name, today, the environments that would break if
hourswere removed tonight.
Follow-up
- Telemetry says one client-managed environment still reads
hoursand its contact has not replied in eight weeks. What do you do, and what do you refuse to do? - A client insists on pinning to a version for three years. What do you actually commit to supporting, and how does that appear in the contract?
Reports intermittently return another client's rows under load
Hard · tenant isolation · row-level security · connection pooling · concurrency
A shared-pool deployment scopes every query with row-level security: the application runs SET app.client_id at the start of each request, and policies compare client_id against current_setting('app.client_id'). Twice this week a client's report contained rows belonging to a different client. Staging cannot reproduce it, single-request replays are clean, and the SQL text is identical in the good and bad cases. Connections come from a pool shared by all requests. Give the ordered diagnosis, the reproduction, and the fix.
Approach
- Order the hypotheses by what the evidence already excludes. Identical SQL text and clean single-request replays rule out a missing predicate in that endpoint, so the variable is not the query, it is the value the policy reads when the query runs. Check, in this order: is RLS enabled and forced on the table; is the serving role actually subject to it; and only then, what does current_setting return at execution time.
- Confirm the policy is in force before blaming it. Policies do not apply to the table owner unless FORCE ROW LEVEL SECURITY is set, and never apply to a role with BYPASSRLS or to a superuser. If traffic is served by the owning role, the policy has been decorative all along and the leak is not intermittent at all, only intermittently noticed.
- Instrument the value, not the code. As the first statement inside every transaction, read current_setting('app.client_id', true) and compare it with the tenant on the request, emitting a counter on mismatch. A defect invisible to replays shows up here within minutes of real concurrent traffic.
- Explain why pooling makes it intermittent. A plain SET is session-scoped and survives COMMIT, so the backend returns to the pool carrying the previous request's value. This needs no transaction-pooling proxy; an ordinary application connection pool is sufficient. The next checkout that sets the value late, or skips it on some path, inherits the previous tenant, and row-level security then filters flawlessly for the wrong client.
- Reproduce deterministically rather than by load-testing and hoping. Cap the pool at one connection, issue two requests for different tenants back to back, and have the second issue its first query before its SET. Zero rows would be the fail-closed outcome; the other tenant's rows is the leak, and the difference between those two results is exactly whether the previous value was left behind.
- Fix in layers. Use set_config('app.client_id', $1, true) or SET LOCAL inside the transaction so the value dies at COMMIT; assert it matches the request immediately before the first query rather than trusting an upstream middleware; set FORCE ROW LEVEL SECURITY; and serve traffic with a role that neither owns the tables nor holds BYPASSRLS, reserving the owning role for migrations.
Follow-up
- You owe a breach notification within contractual time. What query bounds which client pairs and which rows were exposed, and what in the schema makes that answerable at all?
- A background job has no inbound request and therefore no tenant. Where does its tenant come from, and what stops it from being where this happens next?
- How does the same failure mode look on the dedicated and client-managed tiers, and what does that tell you about where isolation should live?
Suggested practice checklist
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Day 1 — Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Day 2 — References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Day 3 — Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Day 4 — Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Day 5 — Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Day 6 — Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Day 7 — Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
Scope is read from what you decided
Team size, service count and tickets closed say very little. Seniority shows in the decision you owned: what you chose not to build, which constraint you traded away, whose objection you had to resolve before anything could move. A large project where you executed someone else's plan is a small story.
Unblock an engineer stuck on an intermittent connector
Easy · mentoring · failure classification · debugging method
An engineer has spent two days on a connector that intermittently applies nothing: some runs read records and apply zero, others are clean, and they have been adjusting retry settings between runs. Describe how you have actually unblocked someone in this position. The account should show what you asked before you suggested anything, the method you handed over rather than the fix you found, and how you checked a week later that the method stuck rather than just the one bug.
Approach
- Start with what you asked, not what you knew: which failure classes those runs carry, whether records_read and records_applied differ and by how much, and whether the observed source schema version changed. Those three separate a dedupe hit from a rejected write from a client-side throttle.
- Hand over the habit rather than the answer: write the candidate causes down (credential expiry mid-run, schema drift failing validation quietly, the client's gateway returning 200 with an empty body, dedupe suppressing everything after a key derivation change), then pick the single observation that eliminates each.
- Name what you deliberately did not do, which is take the keyboard, and be honest that this is slower in the moment.
- Say how you verified the method transferred: a later, unrelated failure they classified before changing anything, or a runbook they wrote that somebody else used.
- Mention the structural change the episode justified, such as recording failure_class by owner so that 'intermittent' becomes a bucket with a count rather than an adjective.
Follow-up
- Same engineer, but the client's change window closes in three hours. What changes, and what do you give up by changing it?
- How do you distinguish someone who is stuck from someone who is struggling productively, before you intervene?
Ship a connector on a deadline with named debt
Medium · delivery pressure · risk bounding · idempotency
Describe shipping something you knew was incomplete against a fixed external date: a client change window, a month-end, a contractual go-live. Name the specific shortcut, such as an ambiguous outcome classified as a plain failure, a watermark advanced outside the ledger transaction, or an unchunked backfill. State the bound you put around it, who accepted the risk and where that was recorded, what would have made you refuse to ship, and what actually happened to the debt afterwards. Do not describe debt you never wrote down.
Approach
- Name the shortcut precisely enough that its failure mode follows from the description: no ambiguous state, so a timeout retried and duplicated; or the watermark committed in a different transaction from the ledger row, so a crash between them loses or replays a range.
- Give the bound, which is what separates a judged risk from a gamble: a first run restricted to a reversible slice, a record cap, a kill switch, or a reconciliation read against the target scheduled before the next run.
- Say who accepted the risk and in what artefact. 'The client knew' is credible only with a written record and a date.
- State your refusal line and why this fell on the shipping side of it. Irreversibility is the usual test: damage you can undo ships, silent corruption inside a client's system does not.
- Report what happened to the debt with a date, including the case where it is still live, and what it has cost since.
Follow-up
- The shortcut fires in production. Walk through the first hour: what you look at, in what order, and what you tell the client.
- Same deadline, but the shortcut risks writing duplicates into the client's system rather than into yours. Does your answer change?
Disagree in review about a dedupe key derivation
Medium · idempotency · code review · schema drift
A colleague's connector change computes its dedupe key as a hash over the whole source payload. You believe it must be derived from named, stable source fields with a stored derivation version, because the client renames and retypes fields without notice and every previously computed key must stay reproducible. Recount a code review disagreement of this shape that you actually had. Give the argument you made, the evidence that moved it, how long it stayed open, and what you did when the author was still unconvinced.
Approach
- State the technical claim as a failure rather than a preference: hashing the whole payload means any upstream field addition changes every key, so the next run finds no ledger hit and re-applies records the client already has.
- Say what evidence you produced. A replay of a real past payload change through both derivations, or a count of fields in the sample that have already changed shape, wins a review; seniority does not.
- Describe the boundary you used between disagreeing and committing: reversible choices get one round and then the author decides, while a choice that corrupts persisted data is worth escalating. Say which this was and why.
- Include the cost you accepted. A versioned derivation means storing the input field names and the derivation version alongside the hash, and keeping each old version's code path alive as long as its keys can still be looked up.
- Close with what the review changed beyond the one diff: a test that pins the key across a renamed field, or a written rule about what may feed a dedupe key.
Follow-up
- The client renames a field that feeds the key. Walk through what your derivation version does on the next run, and what happens to records already applied under the old key.
- The author was senior to you and unconvinced, and the merge was needed that afternoon. What did you do?