Behavioural Questions
Behavioural questions at senior/staff level are not about "tell me about a time you worked in a team." They are about engineering judgment, technical leadership, and how you navigate ambiguity and tradeoffs. The answers below use Stratos Wallet as the evidence base.
Tell me about a hard architectural decision you made
Senior
The most interesting one in Stratos Wallet was the balance storage decision.
The simple approach is to store a balance field on the account record and update it on every transaction. It's faster to read, simpler to implement, and how most tutorial apps work.
The principled approach is to never store the balance: derive it from an append-only ledger every time it's needed.
I chose the ledger approach, but not because it's more interesting. I chose it because of a specific failure mode the stored-balance approach can't handle: two-write inconsistency.
A transfer involves two writes: update the balance, write the transaction record. If the server crashes between them, you end up with a balance that disagrees with the transaction history. In a financial application, there is no safe way to recover from this without a full audit. The balance is wrong, the history is wrong, and you can't tell which one is authoritative.
The ledger approach eliminates this class of bug structurally: the balance is computed from the transaction records, so they literally cannot disagree. The cost is O(n) computation per balance read, which requires periodic snapshotting at scale, but that's a performance problem with a known solution, not a correctness problem with no solution.
The decision came down to: which problem would I rather have? A performance challenge I know how to solve, or a consistency bug that could require a regulatory disclosure? The answer was straightforward.
Tell me about a bug that was hard to find
Senior
In an earlier version of the wallet code, balance history was returning empty for all users except the original demo user. No error, no exception: just an empty array.
The root cause took a while to find. There were two separate data structures for wallets: a static array (seeded at module initialisation) and a Map (populated at runtime when users registered). computeBalanceHistory() searched the static array. New user wallets went into the Map. The two stores never interacted.
The bug was invisible during development because I always tested with the original demo user, whose wallet was in the static array. The first time a colleague registered a new account and checked balance history, it was empty, and there was no error to follow.
What it taught me: the most dangerous bugs are the ones that fail silently for a specific class of inputs. If computeBalanceHistory() had thrown when the wallet wasn't found, I would have caught it immediately. Instead it returned []: a valid empty result that looked like correct behaviour for an account with no history.
The fix was structural: remove the static array entirely, seed the Map at startup, and route all wallet access through a single getWalletById() function. Making it structurally impossible to have two diverged stores means that class of bug can't happen again, not just in this case, but ever.
How do you approach an unfamiliar codebase?
Staff
Week 1: understand the system, not the code.
I start with the data model, not the components. What are the core entities? How do they relate? Where does state live? A financial app's data model tells me more about the system's complexity than any component tree.
Then I read the ADRs or equivalent decision logs if they exist. I want to understand why the system is the way it is before I form opinions about whether it should be different.
I trace one user action end-to-end: a form submission, a navigation event, a real-time update. What happens at each layer? Where does it enter the frontend, how does it flow through state, what does it render?
Week 2: find the seams.
What are the boundaries between features? Where does state leak across what should be domain boundaries? Where are the implicit dependencies that aren't visible in import statements?
I look for the "everyone knows not to touch this" files: the ones that are imported everywhere, that have grown without a clear owner. These are often where the most debt lives.
What I don't do:
I don't start proposing rewrites in week 1. I don't assume the existing decisions were wrong. Most codebases that look messy have reasons behind the mess: constraints, historical context, deliberate tradeoffs that aren't visible without asking. Earn the right to have opinions by understanding the existing decisions first.
By the end of week 2, I want to be able to explain the system's architecture to a new engineer and identify two or three specific, scoped improvements with clear reasoning. Not a rewrite. Scoped improvements.
How do you balance engineering quality with shipping speed?
Staff
I reframe the conversation. "Done properly" is often ambiguous: it can mean anything from "has tests" to "handles every edge case we'll encounter in three years." The question is not "quality versus speed" but "which quality attributes matter right now and which can be deferred?"
My process:
Define the non-negotiables. In a financial application: correctness, security, and idempotency are not negotiable. I won't ship a transfer feature that can double-charge. But I might defer comprehensive test coverage, accessibility polish, and monitoring instrumentation.
Make the tradeoffs explicit. "We can ship in two weeks if we defer test coverage and accessibility. The risk is: if there's a bug in the transfer logic, we won't catch it before it reaches users, and fixing it under production pressure is more expensive than doing it right now." I document this.
Build for replaceability, not perfection. Two-week implementation doesn't mean cowboy code. It means vertical slices so the feature is self-contained and replaceable. It means idempotency and rollback from day one. It means the correctness properties are solid and the polish is deferred. Code written in two weeks with clean boundaries is much easier to harden later than code written in two weeks that's woven through every other module.
Follow through on the deferral. The technical debt is real. I put it in the backlog with a clear description of what was deferred and why, and I advocate for the time to address it in the next sprint. The failure mode is not cutting corners: it's cutting corners and pretending you didn't.