cswork / libraryOpen practice workspace

Interview guides · Axos Business Center

Axos Business Center Software Engineer Interview Questions & Guide 2026

Testing practice for Axos Business Center software engineer candidates: the boundaries worth covering, and why flakiness is a real defect.

The 60-second brief

The Software Engineer role at Axos Business Center is a critical function within the organization, tasked with building and maintaining the robust digital infrastructure that supports modern banking operations. You will be responsible for translating complex business requirements into scalable, reliable software solutions that directly impact the efficiency of Axos Business Center products.

If the seat owns a service boundary, scope your preparation toward failure behaviour rather than topology. Retrying over an at-least-once channel produces duplicates by construction, so a retry policy is only as safe as the idempotency key underneath it.

Focus areas

  • Consume webhooks duplicated, delayed and out of order
  • Name the isolation level each invariant requires
  • Model money movement as balanced double-entry postings

Overview

This role is not merely about writing code; it is about understanding the intersection of financial logic and technical implementation. Whether you are working on backend systems, data processing, or internal applications, your work ensures that the bank’s services remain secure and performant. You will collaborate closely with cross-functional teams, including product managers and operations, to ensure that the technology stack aligns with the fast-paced needs of the financial services industry.

The Axos Business Center interview process

Axos Business Center candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Recruiter Screen

Half of this call is the part candidates treat as small talk: start date, notice period, work authorisation and its timing, location and time zone, on-call, and the number. Those are what kill offers late, after several engineers have each spent a day. Surfacing a hard constraint now costs you nothing and occasionally buys you something, since a loop compressed to fit a competing deadline can usually only be arranged if it is asked for early. The common failure is deflecting the compensation question twice, then discovering at offer stage that the band never reached your number.

What to demonstrate

  • Whether your hard constraints are compatible with the role before a loop gets booked: earliest start, notice period, what authorisation you hold and when it needs action, days on site, willingness to carry a pager
  • Whether you give a compensation range with something behind it, such as current total compensation or a competing timeline, rather than leaving the band untested
  • Whether your stated timeline is real, since a competing deadline raised now is something scheduling can sometimes work around and the same deadline raised at offer stage usually is not

How to prepare

  • Write each constraint down in one line before the call and state them as facts rather than negotiating them live under a question you were not expecting
  • Set your range from two or three current data points for that level and location, and name the structure you are quoting in, so the number is comparable to the one they are holding
  • If another process is running, say where it stands and by when, and ask directly whether this loop can be scheduled inside that window

Hiring Team Rounds

When a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.

What to demonstrate

  • Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
  • Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
  • Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
  • Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened

How to prepare

  • Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
  • Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
  • Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure

Take-home Assignment

A test suite is read as a statement about what you believe can break, which is why the selection matters far more than the count. Assertions that repeat the happy path with different numbers tell a reviewer nothing. Cases at the boundaries tell them where you looked: empty input, one element, duplicates, a malformed row, a value at the edge of the accepted range. The sharpest question to ask of a test you have just written is whether it would fail if the logic were wrong. If deliberately breaking the function leaves the suite green, that test is documentation rather than a check.

What to demonstrate

  • Which cases you chose, and whether the suite reaches empty, single, duplicate and malformed inputs instead of the prompt's example with the numbers changed
  • Whether the suite is deterministic and runs offline, so it produces the same result on a reviewer's machine as on yours
  • Whether assertions are written against behaviour rather than internals, so a reasonable refactor does not force the tests to be rewritten
  • Whether a failure is readable on its own, naming the input and the expected value rather than reporting a bare assertion

How to prepare

  • Mutate your own code by hand before submitting: change a < to <=, drop the last iteration of a loop, return the input unchanged. Anything that stays green is a missing test, and this takes minutes
  • Remove nondeterminism before writing assertions. Pass the clock in rather than calling now() inside the logic, seed any random generator explicitly, and do not assert on the iteration order of a Go map or a Python set, since Go randomises that order deliberately and Python salts string hashing per process unless PYTHONHASHSEED is pinned
  • Time a full run of the suite and trace anything slow to its cause, usually a sleep or a real network call, then replace it with a fake so the reviewer is not waiting

Cognitive Assessment

What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.

What to demonstrate

  • Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
  • Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
  • Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly

How to prepare

  • Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
  • Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
  • Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live

Where candidates lose points

Retrying a charge after a timeout

A timeout is not a failure; it is an unknown outcome, and the request may have been processed in full with only the response lost. Re-sending it without an idempotency key that the processor itself honours produces a duplicate charge, which is a customer-visible incident and usually a dispute. The correct handling is to treat the state as unknown, query the processor for that key or client reference, and only then decide. The mechanism also depends on the key being generated once by the caller and reused across every attempt — generating a fresh key per retry turns the whole scheme into a no-op while leaving all the code that appears to implement it in place.

Writing the state change to the database and publishing the event in the same code path

No transaction spans a relational database and a message broker, so a crash between the two leaves one done and the other not, and the failure is asymmetric in both orderings: publish-then-commit invents events for state that never existed, while commit-then-publish silently loses events for state that does. Retrying the publish after the commit is not a fix, because the process can die before the retry runs. The working shape is an outbox row written inside the same transaction plus a relay that publishes it at least once, which makes consumer-side idempotency mandatory rather than optional. Note also that 'exactly-once' in a stream processor means exactly-once processing within that system's own read-process-write transaction, and says nothing at all about an external side effect such as charging a card.

Answering a debugging question with a guess instead of a bisection

Give a procedure that halves the search space at each step: confirm the symptom reproduces, establish the last known-good version, input or timestamp, then bisect over commits, over the data, or over the layers of the request path. A plausible cause with no way to confirm it is the same move whether it happens to be right or wrong, which is why it scores nothing.

Treating a network call as though it were a local function call

A remote call can be slow, fail, or return after you stopped waiting, so name the timeout, the retry policy, and what the caller sees while the dependency is down. A call with no timeout turns one slow dependency into an exhausted thread or connection pool in every service upstream of it.

Questions & practice

What are the K values for EIGRP, what is the default value, and what is its purpose?

Medium · data structures and algorithms

Approach

  1. Choose the data structure from the access pattern, not from familiarity.
  2. State the target complexity and say which constraint rules the naive version out.
  3. Name the brute-force solution and its complexity before improving on it.

Follow-up

  • Which test case would catch an off-by-one here?
  • How does this change if the input no longer fits in memory?

What specific technology stacks have you used in your previous roles?

Medium · data structures and algorithms

Approach

  1. State the target complexity and say which constraint rules the naive version out.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. Walk one small example through your approach before writing the whole thing.

Follow-up

  • Which test case would catch an off-by-one here?
  • What is the worst case, and how likely is it on real data?

Detect duplicate-charge bursts in an out-of-order authorisation stream

Hard · sliding window · event time · watermarks · deduplication

Authorisations arrive as (instrument_token_id, amount_minor, currency, event_time, arrival_time) at roughly 3,000 per second, up to 60 seconds late and out of order. Flag any token with three or more authorisations of identical (amount_minor, currency) inside any 10-minute window of event time. Report each flag once, as early as correctness allows. State memory per key and in total, how late an event you will accept, and what you do with one that arrives after you have already reported — or already declined to report — that window.

Approach

  1. Key state by (instrument_token_id, amount_minor, currency), not by token: the predicate is about identical amounts, so the window belongs to the triple. Each key holds its own event-time-ordered deque.
  2. In-order, this is two pointers: on insert, pop from the front while front <= new - 10 min, then flag if the deque reaches length 3. Amortised O(1) per event, memory proportional to that key's window occupancy.
  3. Out-of-order arrival breaks append-only monotonicity, so insert in position instead. With lateness bounded at 60 seconds the insertion point is always near the tail, so a short sorted vector or a 600-bucket per-second ring keeps it O(w) with tiny w; a balanced tree per key is correct but over-built for a one-minute reorder.
  4. Drive decisions off a watermark of max(event_time seen) - 60 s, never off wall clock. Anything older than the watermark is too late to change an answer and is counted in a late_dropped metric. Without an explicit watermark you have still chosen a lateness policy — you just cannot state it or test it.
  5. Report-once needs its own state: per key, a set of already-flagged 10-minute window ids. A later event inside an already-flagged window must not re-flag, and a late event that completes a window you never flagged must flag — which is why state is retired at window close plus the lateness bound, not at window close.
  6. Size it: state lives 660 seconds, so at 3,000 events per second there are about 1.98 million in flight, plus one flagged-window set per active key. Bound total memory explicitly and shed by key age, and say what shedding costs — a shed key can miss a flag, making the threshold a product decision rather than a tuning knob.

Worked solution (40 min)

  1. Write the key, the window, the watermark and the report-once rule in four lines before any code; every later bug is one of these left implicit.
  2. Implement per-key state as a sorted deque of event times plus a set of flagged window ids, both retired at watermark - 660 s.
  3. Fixture 1, in order: identical amounts at t, t+1 min and t+9 min produce one flag. A fourth at t+11 min evicts t+1 exactly under the half-open rule pop while front <= new - 10 min, leaving two events and no second flag; change that comparison to strict < and the same input flags twice. Pick one and write it in the spec.
  4. Fixture 2, out of order: deliver the same three events as t+9, t, t+1 and assert the flag fires on the third arrival with the same window id as fixture 1.
  5. Fixture 3, too late: the t event arrives 90 seconds after the watermark passed it, so no flag fires and late_dropped increments by one.
  6. Replay all three fixtures under twenty random arrival orders and assert the flag set is identical whenever every event is inside the lateness bound.

Expected result: Fixture 1 flags exactly once, at the t+9 event; the t+11 event does not flag under the half-open rule and does under the closed one, which is the boundary the fixture exists to pin. Fixture 2 yields the same single flag with the same window id. Fixture 3 yields no flag and late_dropped == 1. All twenty shuffles produce an identical flag set.

Verify it yourself

  • Replaying the same events in any arrival order within the lateness bound produces identical flags — the only property that makes this job re-runnable.
  • Per-key state never exceeds that key's events within 660 seconds; assert it rather than hope.
  • Every flag's window id derives from event time alone; arrival_time appears only in the watermark computation and the lateness metric.

Follow-up

  • Make the threshold and window configurable without rebuilding all in-flight state on every change. What does that constrain in the data structure?
  • Two stream partitions hold events for the same token. What does that force the partitioning key to be?
  • An event arrives three hours late. Does any answer change, and who is told?

Store multi-currency amounts and prove each transaction balances

Easy · minor units · normalisation · constraints

ledger_entry holds entry_id, transaction_id, account_id, direction (debit or credit), amount_minor (bigint, CHECK > 0), currency char(3), source_type, source_id, business_date and posted_at. The service posts in JPY, USD and KWD. Specify the column types for money and the reference table that carries each currency's ISO 4217 minor-unit exponent, and say why that exponent must not be a constant in application code. Then write the query that returns every transaction_id whose entries fail to sum to zero within each currency, ordered by the largest absolute imbalance.

Approach

  1. State the representation: amount_minor is an integer count of minor units and the scale lives with the currency, not in the column. int4 tops out at 2,147,483,647 minor units, which is about 21.5 million units of a 2-exponent currency, so bigint, and the currency code travels on every row that carries an amount.
  2. Normalise the scale into currency(code char(3) primary key, exponent smallint, name) and join it only at display and parse boundaries. The exponent is 0 for JPY and KRW, 2 for USD and EUR, 3 for KWD, BHD, JOD, OMR and TND, so a hard-coded divide by 100 is wrong by 100x for JPY and by 10x for KWD, in opposite directions.
  3. Rule out binary floating point outright: IEEE 754 binary64 cannot represent 0.1, so repeated accrual drifts by a few minor units that later appear as reconciliation breaks. If a fixed-scale decimal is used instead, pin the scale per currency and name the single place rounding happens.
  4. Write the balance check as a grouped aggregate: sign the amount with a CASE on direction, GROUP BY transaction_id, currency, and HAVING the signed sum <> 0. The sign belongs in direction, and the CASE is the one place it becomes arithmetic.
  5. Then order in a second layer, because the ordering the question asks for cannot be written on the grouped query itself. PostgreSQL lets an output alias stand alone in ORDER BY but resolves anything inside an expression against the input columns, so ORDER BY abs(imbalance_minor) DESC raises SQLSTATE 42703, column "imbalance_minor" does not exist, while the bare ORDER BY imbalance_minor DESC is accepted and sorts signed rather than absolute. Wrap the grouped query and order by abs() outside it, or repeat the aggregate as ORDER BY abs(SUM(CASE ...)) DESC, which is legal because aggregates are allowed there.
  6. Group per (transaction_id, currency) rather than per transaction, because a cross-currency movement balances only within each currency, bridged by an explicit FX position account whose rate, source and timestamp are stored on the transaction.
  7. Note that no CHECK constraint can express this, since it spans rows: enforce it at write time with a DEFERRABLE INITIALLY DEFERRED constraint trigger that fires at commit, or by routing every posting through one function, and keep this query as the independent audit.

Follow-up

  • A transaction has a USD leg and a JPY leg. Which accounts appear, and where does the rounding residual land?
  • How would you enforce the zero-sum rule at write time without serialising all postings behind one lock?
  • Someone proposes storing a display amount alongside amount_minor. What goes wrong at the first rate change or rounding rule change?

Make a charge endpoint safe under concurrent duplicate retries

Easy · idempotency · unique constraints · read committed

idempotency_key holds id, scope, key, request_fingerprint (SHA-256 over the canonicalised body), status (in_progress, completed, failed), response_status, response_body, locked_at, completed_at, expires_at, created_at. Fifty identical create-payment requests carrying the same scope and key reach four application instances inside the same 20 ms. Give the DDL constraint and the exact statements the handler runs so that exactly one payment_intent is created and all fifty callers receive the same response body. State what you return when that key arrives with a different fingerprint, and what an arrival after expires_at means.

Approach

  1. Put the concurrency control in the schema: UNIQUE (scope, key). A SELECT-then-INSERT cannot work because both transactions can read nothing before either commits, so the check passes twice and the constraint then surfaces as an error on a payment that succeeded.
  2. Claim the key with INSERT ... ON CONFLICT (scope, key) DO NOTHING RETURNING id. A conflict returns zero rows rather than the existing row, so branch on rowcount: the winner proceeds, the loser reads the stored row.
  3. Keep that path on READ COMMITTED deliberately. The loser's follow-up SELECT takes a fresh statement snapshot and therefore sees the winner's committed row; under REPEATABLE READ the transaction snapshot predates that commit, the row stays invisible and the loser concludes the key does not exist.
  4. Split the work across two transactions because the processor call cannot sit inside one: commit the in_progress row with locked_at first so losers can see a claim, perform the effect, then write payment_intent plus status=completed with response_status and response_body in a single second transaction.
  5. Handle the crash window explicitly: a row stuck in_progress past its lease is an unknown outcome, not a failure, so the reaper queries the processor for that key before deciding. A loser that sees in_progress returns 409 and retries rather than repeating the effect.
  6. Compare request_fingerprint before replaying anything. Same key with a different body is 409, never the cached response, because replaying confirms a payment the caller did not request; and set expires_at beyond the client's and the processor's maximum retry horizon, since a replay after it is a genuinely new request.

Worked solution (20 min)

  1. Create idempotency_key with UNIQUE (scope, key) and write the claim statement as INSERT ... ON CONFLICT DO NOTHING RETURNING id, plus the fallback SELECT on zero rows.
  2. Drive it with 50 threads sending the identical body, and assert one payment_intent row and fifty byte-identical response bodies.
  3. Re-send the same key with amount_minor changed by one unit and assert 409 plus no new payment_intent row.
  4. Kill the process between the processor call and the second commit, restart, and show the reaper resolves the in_progress row by querying the processor rather than by re-charging.

Expected result: One payment_intent row, one idempotency_key row at status=completed, fifty responses with identical response_status and response_body, zero 5xx. A 51st request with the same key and a changed amount returns 409 and leaves payment_intent untouched.

Verify it yourself

  • Repeat the 50-way race 20 times; COUNT(*) FROM payment_intent for that key is 1 on every run.
  • Assert the fifty bodies are byte-identical, not merely all 2xx.
  • Drop the UNIQUE constraint and confirm the test now fails; if it still passes, the race is not being exercised.
  • Re-run the loser path under REPEATABLE READ and observe it failing to find the winner's row, which is why the level is pinned.

Follow-up

  • The handler dies after the processor call and before the local commit. What does the next retry with that key observe, and how does the system converge on exactly one charge?
  • Does the downstream processor honour an idempotency key of its own? Who mints it, and what breaks if a fresh one is generated per attempt?
  • How do you purge rows past expires_at without the delete contending with the insert path?

Webhook intake under duplicate, unordered delivery and ledger outage

Medium · webhooks · ordering · backpressure · availability

A processor POSTs 5,000 signed events/s to one endpoint, at least once, unordered, retrying for 24 hours until it gets a 2xx, and treating a response slower than 10 seconds as a failure. Each event carries a provider event id, an object id and an object version. Your ledger is occasionally unavailable for minutes at a time. Design intake: signature verification, deduplication, how a succeeded event arriving before the processing event it follows is applied, and what status you return while the ledger is down. Justify that status and bound the backlog.

Approach

  1. Verify before parsing. Compute HMAC-SHA256 over the timestamp concatenated with the raw request bytes, compare in constant time, and reject a timestamp outside a tolerance of a few minutes so a captured request cannot be replayed indefinitely. Parsing to an object and re-serialising before verification is how a signature check silently stops working the day a JSON library reorders keys or renormalises a number.
  2. Make 'accept' mean 'durably recorded', not 'applied'. Insert (provider_id, provider_event_id, raw body, received_at) under a UNIQUE constraint on the provider's event id and return 2xx in single-digit milliseconds; apply asynchronously. The endpoint doing no business logic is what keeps it inside the provider's timeout at 5,000/s, and the constraint handles the at-least-once repeats without application logic.
  3. Order by version, never by arrival: UPDATE payment_intent SET status = $s, version = $v WHERE intent_id = $1 AND version < $v. A stale succeeded-then-processing pair updates zero rows on the second event and is recorded as discarded. Arrival time and the provider's own created timestamp are both unusable for ordering, because retries reorder both.
  4. Split the failure case by which store is down, because the answer differs. If acceptance storage is healthy and only the ledger is down, keep returning 2xx: you hold the durable record and version-ordered application makes the replay safe, so availability costs you nothing but lag. If acceptance storage itself is down, return 5xx and let the provider's 24-hour retry schedule be your queue; a 2xx you cannot honour is an event the provider will never send again.
  5. Bound the backlog explicitly in rows or in lag-minutes, alarm on it, and shed to 503 past the bound rather than accepting work you cannot drain. The health metric that matters is per-object version gaps, not queue depth: a queue can be empty while an object is stuck three versions behind.

Worked solution (25 min)

  1. Build the receiver: constant-time HMAC over timestamp plus raw body, a tolerance window, then an INSERT under UNIQUE (provider_id, provider_event_id) and an immediate 200 with no application.
  2. Replay a captured request outside the tolerance window and assert rejection; replay it inside the window and assert one stored row rather than two.
  3. Deliver succeeded at version 7 before processing at version 5 and apply both through the version-conditional UPDATE.
  4. Stop the ledger, drive 5,000 events/s for five minutes, and watch endpoint p99 and backlog growth.

Expected result: The out-of-order pair leaves the object at version 7 in the succeeded state; the version-5 update reports zero rows affected and is recorded as discarded rather than raised as an error. During the ledger outage the endpoint keeps answering 200 in single-digit milliseconds while the backlog grows at about 5,000 rows/s, roughly 1.5 million rows over the five minutes, and drains afterwards with nothing lost.

Verify it yourself

  • Verification runs on raw bytes: confirm by re-serialising the parsed body with a different key order and watching the signature fail.
  • Deduplication is keyed on the provider's event id, not on a hash of the body, since a provider retry may differ in whitespace.
  • Killing the process with events accepted but unapplied loses nothing, because acceptance was a committed row.

Follow-up

  • After a four-hour outage the provider replays everything. What does your consumer do with 70 million duplicates?
  • One hot merchant object receives 50 events/s. Does the version-conditional update starve or livelock, and what do you change?
  • You detect a permanent gap at version 12 for one object. How do you close it?

Paginate an append-only statement endpoint under continuous writes

Easy · pagination · keyset · indexing

Statement reads come from ledger_entry (entry_id bigint monotonic primary key, transaction_id uuid, account_id bigint, direction, amount_minor bigint, currency, source_type, source_id, business_date date, posted_at timestamptz). The caller is a merchant dashboard fetching one account newest-first, 100 rows per page, while new entries are still being appended. Offset pagination is in production and support reports rows appearing on two consecutive pages. Design the replacement: the index, the exact WHERE clause and ORDER BY, the cursor's contents and encoding, and what happens when a caller reuses a cursor an hour later.

Approach

  1. Name the mechanism of the duplication rather than blaming the sort. Each page is a separate statement with its own snapshot; under a descending order, every row inserted between page 1 and page 2 shifts the whole result one position later, so the last row of page 1 reappears as the first row of page 2. The cost is bad too: OFFSET n scans and discards n rows, so deep pages are O(offset + k).
  2. Replace it with a keyset predicate over the unique, monotonic ordering column: WHERE account_id = $1 AND entry_id < $cursor ORDER BY entry_id DESC LIMIT 100. That is one index descent plus 100 rows, O(log n + k), and the cost does not change with depth.
  3. Index (account_id, entry_id). A B-tree is scanned in either direction at the same cost, so a separate DESC index buys nothing. If the endpoint also filters on a date range, the tuple becomes (account_id, business_date, entry_id) and the predicate becomes a row-value comparison, (business_date, entry_id) < ($d, $e), which still resolves to a single descent.
  4. Make the cursor opaque and self-describing: base64 of the anchor tuple plus a hash of the filter set and an encoding version. A cursor presented with a different account or date range is rejected with a distinct code rather than quietly returning another account's rows.
  5. State the validity rule. Because the table is append-only, the anchor row is never deleted, so a keyset cursor stays valid indefinitely and an hour-old cursor simply resumes. Add one caveat for machine consumers: entry_id is allocated before commit, so an entry with a lower id can become visible after a reader has already passed it. A human statement page never notices, but a feed consumer needs a lag window or a commit-ordered marker rather than the newest id.

Follow-up

  • The dashboard now wants to sort by amount_minor descending. What breaks, and what does the cursor have to contain to fix it?
  • A caller asks for a total page count. What does that cost on a 400-million-row table, and what do you offer instead?
  • How would you support paging backwards from a cursor without re-sorting in the application?

Statement endpoint slows linearly with the entry count

Easy · n+1 · latency · query planning

A statement endpoint reads ledger_entry for one account and a date range and returns entry_id, amount_minor, currency, source_type and business_date, plus the display name of the counterparty account found through the other entry on the same transaction_id. p99 is 40 ms for a 20-entry month and 2.1 s for a 900-entry month. The database reports 901 statements per request, each under 1 ms, and no single query is slow. Diagnose the cause and give the fix, stating the statement count and the p99 you expect afterwards.

Approach

  1. Establish the shape before theorising. Divide the per-request statement count from pg_stat_statements by the rows returned: 901 statements for 900 rows is one driver query plus one per row, and because each is sub-millisecond it rules out a bad plan. The latency is round trips, not work.
  2. Identify the per-row statement by its normalised text. It will be a single-row lookup on ledger_entry joined to account, keyed by transaction_id, issued from the serialisation layer rather than the repository. Confirm it is lazy loading by checking that it disappears when the counterparty name is dropped from the response.
  3. Do the arithmetic as a division from the incident rather than a multiplication from assumptions. The 900-entry request issues 880 more statements than the 20-entry one, 901 against 21, for 2.06 s more wall time, so each extra statement costs at most about 2.3 ms end to end, and that is an upper bound because the larger response also carries more payload. Then measure one round trip on this path instead of assuming it: pool checkout, network, parse, bind, execute and per-row hydration in the driver, not network alone. A naive 1 ms network plus the 0.2 ms the database reports would cover only about 1.05 s, half the excess, so if a measured round trip really is that cheap then the N+1 is not the whole story and you owe an explanation for the rest before proposing a fix.
  4. Replace the per-row lookup with one batch statement: collect the transaction_ids from the driver query and fetch all counterparty entries with WHERE transaction_id = ANY($1), then build the map in memory. Two statements per request, independent of the range width.
  5. Check the index before declaring victory. The batch query needs an index on ledger_entry(transaction_id); without it ANY($1) degrades to a sequential scan over the entry table and the fix becomes a worse regression than the bug.

Follow-up

  • What changes if the statement must paginate at 200 entries per page?
  • A transaction can carry more than two entries once fees have their own leg. What does your query return then, and what should the counterparty column mean?
  • What would have caught this before production, given that no individual query is slow?

Suggested practice checklist

Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.

Day 1 — Diagnostic, scored before you study anything

  • Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
  • Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
  • Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.

Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.

Day 2 — Largest gap: find the boundary rather than the subject

  • Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
  • Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
  • Re-attempt one of them from blank four hours later with nothing open.

Deliverable: A sub-skill map with the two blocking sub-skills circled.

Day 3 — Drill the blocking sub-skill by repeating the shape

  • Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
  • State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
  • Have someone else read your one-sentence rule and find the precondition you left out.

Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.

Day 4 — Second gap, plus maintenance on the strongest area

  • Run the same sub-skill decomposition on the second-largest gap in half the time.
  • Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
  • Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.

Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.

Day 5 — The gap that is not a skill

  • Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
  • Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
  • Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.

Deliverable: Two recordings with a counted reduction in time-to-first-question.

Day 6 — Retest under day-one conditions

  • Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
  • For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
  • Write down which single block you would still lose the offer on.

Deliverable: A second scored rubric placed beside the first, with one named remaining risk.

Day 7 — Full loop under interview conditions

  • Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
  • Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
  • Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.

Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.

Know where your numbers came from

Bring the two or three numbers the story rests on and know how they were collected. A p99 whose timer starts inside your handler excludes the time a request spent queued, so it can sit flat while users wait longer. Give the window, the percentile and what the measurement left out, or drop the number.

Can you walk me through your work experience with software development?

Medium · behavioural and engineering judgement

Approach

  1. State the situation in two sentences and spend the rest on the reasoning.
  2. Name the disagreement and how you resolved it with evidence.
  3. Close with what you would do differently, concretely.

Follow-up

  • How did you know your change caused the improvement?
  • What did you decide not to do, and why?

What programming languages did you find more difficult or easier to learn, and why?

Medium · behavioural and engineering judgement

Approach

  1. State the situation in two sentences and spend the rest on the reasoning.
  2. Pick a story where you made the decision, not one where you watched it.
  3. Give the blast radius: what could have broken, and what you measured.

Follow-up

  • How did you know your change caused the improvement?
  • What would you do differently if you ran that again?

Tell me about a time you had to explain a complex technical concept to a non-technical stakeholder.

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. Give the blast radius: what could have broken, and what you measured.
  3. Close with what you would do differently, concretely.

Follow-up

  • What would you do differently if you ran that again?
  • How did you know your change caused the improvement?