Interview guides · Angel One
Angel One Software Engineer Interview Guide
Angel One Software Engineer interview guide: the online assessment, five reported coding and class-design questions, edge cases and a 7-day prep plan.
The 60-second brief
Angel One is a financial services company whose platforms let users manage investments and trade on the stock market. Candidates describe the Software Engineer role as building, maintaining and scaling those platforms. Backend systems that handle large volumes of data, core trading engines and user-facing mobile features are all mentioned, as is day-to-day work alongside product managers and other engineers. For you, that means the assessment code is likely to deal with orders, prices, holdings and transactions, so knowing the vocabulary of trading helps you read a question quickly.
This guide is for candidates applying for a Software Engineer role at Angel One. It covers the one stage candidates report, the Online Assessment, and the five reported questions: a transaction-workflow class structure, a function for real-time market data input, a performance refactor, error handling for high-frequency financial operations, and an extensible class system for trading features. The requirements candidates describe name Java, C++ or Python as example languages, so the examples here stay language-neutral. No later stages are reported, so none are described.
Candidates report a single stage, an Online Assessment, inside a process of about 2-4 weeks. It is described as a check of core programming skills and of implementing classes and functions, with results driven by how many test cases a solution passes.
Focus areas
- Programming for Automated Testing
- Code Correctness
- Object-Oriented Programming (OOP)
Overview
The five reported questions all start from a short, loosely worded requirement and ask you to turn it into working code. Two ask for class structures (user transaction workflows, and a modular system for extending trading features), one asks for a function over real-time market data input, one asks you to refactor existing logic for performance and lower complexity, and one asks for error handling suited to high-frequency financial operations. Because the wording is open, your first job in practice is to pin each one down: pick concrete inputs and outputs, list the states or operations, and write the edge cases before the code.
Candidates report that the assessment is judged objectively by the number of test cases a solution clears, and that missing empty or invalid input handling is a common reason for failing cases. That shapes your order of work: get a plain correct version passing first, then handle empty, zero, negative, duplicate and out-of-order input, and only then tighten the time and space complexity. A clever version that fails a boundary case scores worse than a clear one that passes every case.
Reported preparation advice stresses data structures, object-oriented design (encapsulation, inheritance, modularity) and readable code. In practice that means being able to write, from memory and quickly, a class with a clear public interface and hidden state, a hash map or heap or deque used for the right reason, and small functions with honest names. Money needs care: use integer minor units or a decimal type rather than binary floats, and say so in a comment.
The domain is light but worth an evening. Know what an order is (side, symbol, quantity, price), the difference between a market and a limit order, what a partial fill is, how an average buy price and a holding are updated, and the difference between realised and unrealised profit. Candidates are also advised to learn basic financial concepts so the problem statements are easy to read. Nothing beyond the assessment is reported, so spend your time on code you can write cleanly under time pressure.
The Angel One interview process
Candidates report one stage, the Online Assessment, within a process of about 2-4 weeks. Nothing later is reported, so this guide does not guess at further rounds.
Online Assessment
Candidates describe the Online Assessment as the first and only reported stage, an initial check of core programming skills and of the ability to implement classes and functions. They report that it is judged by how many test cases your solution passes, that empty or invalid input is where many candidates lose cases, and that the questions are representative of the technical challenge rather than a fixed list. The reported topics are programming for automated testing, code correctness, object-oriented programming, implementing classes and function implementation. Approach it in layers: write the class or function signature, make the simplest correct version pass, add boundary handling, then improve complexity. Keep functions small so that a hard problem still yields partial progress, and name things the way a reviewer would expect.
What to demonstrate
- Working code that passes automated test cases, including boundary and invalid input
- Implementing classes with encapsulation, inheritance and modularity
- Function implementation with sensible time and space complexity
- Readable structure and standard naming, which candidates are advised to keep even in an online test
How to prepare
- Build a transaction class with explicit states and validation, then test illegal transitions, insufficient balance and duplicate transaction ids (the transaction-workflow question).
- Implement one market data function, such as a rolling maximum with a monotonic deque and an order book with two heaps, and test empty, out-of-order and duplicate ticks (the real-time market data question).
- Refactor a slow nested-loop function behind tests you wrote first, replacing repeated scans with a hash index, and record the before and after complexity (the refactoring question).
- Write a small exception hierarchy that separates validation, transient and fatal errors, and make retries safe with an idempotency key (the error-handling question).
Where candidates lose points
Using floating-point numbers for prices, quantities or balances
Store money as integer minor units or a decimal type, compare with exact arithmetic, and round only at the output boundary. A test comparing a balance after many small fills will expose float drift that a short manual example hides.
Chasing the optimal solution before a simple one passes
Reported advice favours passing all test cases over untested cleverness. Write the straightforward version first, run your own cases, then replace the slow part (a repeated scan becomes a hash lookup, a repeated sort becomes a heap) and rerun the same tests.
Missing empty, zero, negative, duplicate or unknown-symbol input
Keep a checklist next to every function: empty collection, single element, zero or negative quantity, duplicate id, unknown symbol, out-of-order timestamp. Decide for each whether to reject, ignore or handle it, and write one test per line.
Building one large class that branches on a type string for every trading feature
Define a small interface for the varying behaviour, such as an order type or fee rule, give each variant its own class, and look variants up in a registry. A new feature should mean adding a class, not editing a long if-else chain in code that already works.
Handling errors by catching everything, or retrying an operation that is not safe to repeat
Separate validation errors (never retry), transient errors (retry with a capped backoff) and fatal errors (stop and report). Retry a submit-style operation only when it carries an idempotency key, so a retry cannot apply the same transaction twice.
Questions & practice
Implement a function to process real-time market data inputs efficiently.
Medium · Technical Implementation & Coding
Approach
- Pin down the input first: each tick is (symbol, price, quantity, timestamp), and it arrives as a stream. Keep per-symbol state in a hash map so each tick costs O(1) expected time, and store prices as integers (ticks or paise) to avoid floating-point drift.
- Keep running aggregates rather than recomputing. VWAP is sum(price*qty) / sum(qty), updated in O(1) per tick. Last price, day high and day low are single comparisons.
- For a sliding window (last N ticks or last T seconds), keep a deque and evict expired entries from the front. A monotonic deque gives the window max or min in amortized O(1). Top-k movers across symbols come from a size-k heap in O(n log k).
- Handle the input edge cases explicitly: zero quantity (VWAP divides by zero if every quantity is zero), an unseen symbol, duplicate or out-of-order timestamps, and an empty window when a query arrives before any data.
- State the cost: one pass over the stream, O(1) amortized work per tick, and memory of O(symbols + window size). Never re-scan history on each query.
Follow-up
- How would you return the median price over the last N ticks? (Two heaps with lazy deletion, or an ordered multiset, both O(log N) per update.)
- What changes if one thread receives ticks and another consumes them?
- Ticks can arrive a few milliseconds out of order. How does your window stay correct?
Refactor existing logic to improve performance and reduce complexity.
Medium · Technical Implementation & Coding
Approach
- Lock in the current behaviour before touching anything: write tests that capture today's outputs, including empty input, a single element, duplicates and boundary values. The refactor passes only if those outputs are unchanged.
- Find the real hot spot and give its complexity. The usual case is a nested loop doing membership or lookup checks in O(n*m). Build a hash set or hash map once and the work becomes O(n + m) expected time, paid for with O(m) extra memory.
- Replace repeated work with precomputation: move invariant calculations out of loops, use prefix sums for repeated range totals, and sort once then binary search in O(log n) instead of scanning linearly on every query.
- Reduce complexity in the readability sense too. Use guard clauses instead of deep nesting, a lookup table or map instead of a long if/else chain, and small named functions for each step. In C++, pass large objects by const reference and reserve vector capacity. In Java, build strings in a loop with StringBuilder.
- Check what a faster structure can silently change. An unordered set loses input order, deduplication changes counts, and integer overflow can appear once totals are accumulated. Keep the original order when the output depends on it.
Follow-up
- How do you show that the refactored version returns exactly the same results as the original?
- When is the O(n^2) version acceptable, and what input size makes it fail?
- Your hash-based version uses more memory. When would you choose a sort-based approach instead?
Develop a robust error-handling mechanism for high-frequency financial operations.
Medium · Technical Implementation & Coding
Approach
- Sort failures into classes before writing handlers. Validation errors (bad amount, unknown instrument) are rejected at once and never retried. Business failures (insufficient margin) are returned as results. Transient failures (timeout, dropped connection) are the only ones you retry.
- Retry transient failures with capped exponential backoff plus random jitter and a fixed maximum number of attempts, so many clients do not retry in lockstep. Once the attempts run out, send the operation to a failed or dead-letter queue instead of dropping it.
- Make every operation idempotent with a client-supplied key. A timeout is ambiguous because the order may already have executed, so query its status by that key before resubmitting, and never place it twice.
- Put a circuit breaker (closed, open, half-open) in front of a failing dependency so the system fails fast instead of piling up blocked threads. Keep multi-step updates atomic, or record a compensating action, so a partial failure never leaves one ledger leg without the other.
- Keep the hot path cheap and visible. In C++, return status codes or a result type for expected outcomes and keep exceptions for truly exceptional cases, because throwing is expensive. Never swallow an error silently, and log each failure with a correlation id.
Follow-up
- The exchange call timed out. How do you find out whether the order went through, and what do you do next?
- How do you stop a retry storm when the downstream service recovers?
- How would you test these error paths without a live dependency?
Create a modular class system that allows for easy extension of trading features.
Medium · Technical Implementation & Coding
Approach
- Design for extension through interfaces. An Order holds the common fields (id, symbol, side, quantity), and each order type (Market, Limit, StopLoss) implements a shared interface such as validate() and shouldExecute(currentPrice). A new type is then a new class, not another branch in a switch.
- Register types in a factory or registry that maps the type name to its constructor, so the parsing and execution code never names a concrete class. Adding a feature means one new class plus one registration line.
- Prefer composition to deep inheritance. Run risk checks (quantity limits, price bands, available funds) as a chain of independent rule objects, and publish order events (placed, filled, cancelled) to observers for notifications and audit logging.
- Get the trading rules right in each strategy. A limit buy executes at or below its limit price and a limit sell at or above it. A sell stop-loss triggers when price falls to or below the stop, and a buy stop triggers when price rises to or above it.
- Name the trade-off: every extra layer of abstraction adds indirection, so add an interface only where a second implementation is realistic. Keep each class testable on its own with a mocked price feed.
Follow-up
- How would you add a trailing stop-loss, whose trigger price moves with the market, to this design?
- A new feature needs two behaviours from different classes. Do you use inheritance or composition, and why?
- How do you unit-test a new order type without running the whole execution path?
Write the update path that detects a concurrent edit
Medium · optimistic concurrency · transactional outbox · audit history
resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.
Approach
- One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
- Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
- State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
- Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.
Follow-up
- A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
- Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?
Find version gaps and relay lag with window functions
Medium · window functions · event tables · frame semantics
outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.
Approach
- Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
- Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
- Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
- Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.
Follow-up
- Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
- Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?
Design a class structure that handles specific user transaction workflows.
Medium · Technical Implementation & Coding
Approach
- Name the entities before writing code: Account (id, balance held as an integer in the smallest currency unit, never a float), an immutable Transaction (id, type, amount, source, destination, status, timestamp) and a TransactionService that runs the workflow. Accounts never edit their own history.
- Model the workflow as an explicit state machine, for example CREATED -> VALIDATED -> EXECUTED, with REJECTED and FAILED as terminal states. Allow only listed transitions and throw on any other, so a finished transaction can never run again.
- Put validation behind a Validator interface (amount > 0, source differs from destination, sufficient balance, account active). The service loops over a list of validators, so a new rule is a new class and the service does not change.
- Make the balance check and the debit one atomic step. For a transfer, lock both accounts in a fixed order (lower account id first) so two opposite transfers cannot deadlock, and write ledger entries that only ever get appended.
- Store an idempotency key on each request in a map from key to transaction, so a repeated submit returns the first result. Test the edge cases: zero or negative amount, insufficient funds, a self-transfer, an unknown account and a duplicate key.
Follow-up
- How would you reverse an executed transaction: delete it, or add a compensating entry, and why?
- Two threads transfer A to B and B to A at the same moment. Walk through why your locking does not deadlock.
- Where would a new workflow type, such as a scheduled transfer, plug in without editing the existing classes?
Give the resource write endpoints a failure taxonomy clients can act on
Easy · error taxonomy · status codes · retries
Two callers use POST and PATCH /v1/resources: a server-side SDK that retries automatically, and a browser app that shows the user a message. Today every failure is a 500 carrying prose. Define the error contract for a malformed body, a field that fails validation, an expired token, a token whose auth_version no longer matches, a resource_id owned by another tenant, a version mismatch on update, an idempotency key reused with a different body, an exceeded tenant quota, and a read replica that has not caught up. Deliverable: the envelope, the status per case, and what each caller does.
Approach
- Split the envelope by audience: a stable
codeenum for programs, amessagedocumented as human-only and free to change wording, arequest_idthat joins to resource_revision.request_id and the trace, and adetailsarray of field paths for validation failures. Publish the negative rules too - clients never branch onmessage, and an unrecognisedcodefalls back to the status class. - Assign status by who has to change something. 400 for bytes that do not parse, 422 for a body that parses and violates a rule, 401 for a token that no longer authenticates - expiry and an auth_version mismatch are the same instruction, re-authenticate - 403 for a scope the principal lacks, 404 rather than 403 for another tenant's resource_id because 403 confirms the id exists, 412 for a failed If-Match (409 if the version travels in the body instead), 422 for a key reused with a different fingerprint, 429 for quota.
- Derive retryability from whether the outcome is unknown, not from the status number. A timeout or a 5xx on a write is unknown - the transaction may have committed and the response lost - so the only safe retry is one carrying the same idempotency key. Every 4xx except 429 is deterministic, and retrying it only spends the caller's remaining deadline.
- Refuse to model replica lag as an error. Read-after-write is held by pinning the session to the primary for a short window, not by a 404 the SDK retries into a loop; if staleness must be visible, expose it as a watermark on a 200, because a failure code invites a retry that cannot fix it.
Follow-up
- A customer reports two resources created from one click. Which of your status codes could have produced that, and what in the contract permitted the client's reading?
- You need a tenth error code next quarter without a version bump. What must v1 already have said for that to be non-breaking?
Listing latency scales with page size, not with filters
Easy · n+1 · latency · query batching
The tenant listing endpoint reads resource filtered by tenant_id and status, ordered by updated_at DESC, and returns each row plus the owner's display name from app_user and the actor of that resource's latest resource_revision. p99 is 55 ms at 10 rows per page and 1.4 s at 200. Database telemetry shows 401 statements per request, each under 1 ms, and nothing in the slow-query log. Diagnose the cause and give the fix, stating the statement count per request and the p99 you expect afterwards.
Approach
- Read the counters before forming a theory. 401 statements for 200 rows is one driver query plus two per row, and sub-millisecond execution with an empty slow-query log rules out a bad plan. The time is round trips, which is why it is invisible in every per-query metric and scales with rows returned rather than with filter selectivity.
- Name the two per-row statements from their normalised text: a single-row app_user lookup by user_id, and a resource_revision lookup by resource_id ordered by version DESC LIMIT 1. Confirm by dropping those two response fields and watching the statement count fall to one. That locates the calls in the serialisation layer, not the repository.
- Check that the arithmetic accounts for the whole gap. Measure one round trip to the replica in isolation; 400 trips at roughly 3 ms of network plus 0.2 ms of execution is about 1.3 s on top of a 55 ms baseline, which matches. If the multiplication had fallen short, the N+1 would only be part of the story and you would keep looking.
- Batch both lookups. Collect owner_user_ids and resource_ids from the driver query, then issue WHERE tenant_id = $1 AND user_id = ANY($2) for the users, and PostgreSQL's SELECT DISTINCT ON (resource_id) ... WHERE resource_id = ANY($2) ORDER BY resource_id, version DESC for the latest revision, which the UNIQUE (resource_id, version) index serves directly. On an engine without DISTINCT ON, use a lateral join or a row_number window. Three statements per request at any page size.
Follow-up
- The page size is capped at 200 today. What breaks first if it is raised to 2,000, and is it still this bug?
- How do you stop the next N+1 from reaching production, given that no individual query is slow and the endpoint's tests pass?
Suggested practice checklist
The plan follows the single reported stage and its five questions: vocabulary and test setup first, then one question type per day, then a mixed timed run with behavioral stories.
Day 1 — Vocabulary, language and test harness
- Choose Java, C++ or Python and write a reusable test file with a few assertions and a table of inputs and expected outputs.
- Write a one-page glossary: order, side, symbol, quantity, limit and market price, fill and partial fill, holding, average buy price, realised and unrealised profit.
- Write the edge-case checklist you will reuse all week: empty, single element, zero, negative, duplicate, unknown key, out of order.
Deliverable: A language choice, a working test harness, a one-page glossary and an edge-case checklist.
Day 2 — Class structure for transaction workflows
- Pick a concrete workflow, such as deposit, buy, sell and withdraw, and list its states and the legal transitions between them.
- Implement an account and transaction class with private state, validation, an atomic apply that leaves the balance untouched on failure, and a lookup by transaction id.
- Write at least ten tests covering an illegal transition, insufficient funds, a duplicate transaction id and a failed apply, then compare with the SQL practice question on detecting a concurrent edit.
Deliverable: A working transaction class with ten or more passing tests and a diagram of its states.
Day 3 — Functions over real-time market data
- Implement a rolling maximum and a volume-weighted average price over a stream of (symbol, price, quantity, timestamp) ticks using a monotonic deque and running sums.
- Implement a small order book with two heaps for best bid and best ask, supporting cancel through lazy deletion, and state the cost of each operation.
- Test empty input, a single tick, equal timestamps, out-of-order ticks and a very large batch.
Deliverable: Two functions with complexity written in comments and a passing test file that includes the boundary cases.
Day 4 — Refactor for performance and lower complexity
- Write a deliberately slow function, for example nested loops that match orders to holdings by symbol with repeated recomputation, and add characterization tests first.
- Refactor it using a hash index, precomputed totals, early returns and extracted helper functions, keeping every test green.
- Time both versions on a large generated input and record the before and after complexity; read the listing-latency practice question for a measuring habit.
Deliverable: Before and after code, identical passing tests and a short table of complexity and measured timings.
Day 5 — Error handling for high-frequency operations
- Define an exception hierarchy that separates validation, transient and fatal errors, and return a structured result so callers can act on the category.
- Implement a retry helper with capped exponential backoff and jitter that retries only idempotent calls, and add an idempotency key to a submit-style operation.
- Test with a fake dependency that fails twice then succeeds, and prove that the transaction is applied exactly once; compare with the failure-taxonomy practice question.
Deliverable: An error-handling module with tests showing a retried operation applies once and non-retryable errors fail immediately.
Day 6 — Modular trading features and a portfolio class
- Design an order-type interface with separate classes for market and limit orders, plus a registry, then add a third order type by adding one class and no edits to existing ones.
- Build a portfolio class with dynamic updates: apply a fill, update quantity and average price, handle a sell that closes a position, and return an immutable snapshot.
- Write a short note on where you used encapsulation, inheritance or composition and why.
Deliverable: An extensible class system, a portfolio class with tests, and a diff showing a new feature added without editing old code.
Day 7 — Timed mock and behavioral stories
- Take two of the five questions you chose blind, give yourself a fixed time budget, and work in layers: baseline passing, edge cases, then optimization.
- Review your own failures and list the three edge cases you missed most often.
- Write two short stories, one about a setback and one about explaining a technical choice, using the behavioral practice questions as prompts.
Deliverable: Two timed solutions with a pass and fail table, a list of three missed edge cases and two written stories.
Behavioral preparation for the Angel One loop
The only stage candidates report is a coding assessment, and what they say about the behavioral side is general: be ready to describe your problem-solving process, how you handle setbacks, how you collaborate and how you explain technical reasoning. No specific behavioral questions are reported, so prepare stories that show your own decisions and what you changed afterwards. Keep each story concrete, with the situation, your action and the result you can measure.
- Walk me through how you break down a problem you have never seen before, from the first read to the final test.
- Tell me about a setback on a project: what went wrong, what you did next and what you changed afterwards.
- Describe a time you explained a technical decision to a product manager or teammate who disagreed with it.
- Describe a bug that reached users because of an unhandled edge case. How was it found, and which test now guards against it?
- Tell me about code you refactored. What did you measure before and after, and how did you know behaviour had not changed?
Unblock an engineer without taking the keyboard
Easy · mentoring · leases · at-least-once
A teammate has spent two days on a job handler that occasionally writes duplicate rows. They are certain the queue is delivering twice by mistake. You suspect a lease expiring under a slow handler, so the job is running concurrently with itself. Describe how you have unblocked someone in this position: what you asked before offering a hypothesis, what you showed them rather than told them, and what you left them owning. Then say what you would do if their theory turned out to be the right one.
Approach
- Ask before diagnosing, and ask for things answerable from data they already have: the attempt count on the job rows that produced duplicates, the handler's observed duration against its lease expiry, and whether the duplicate rows share a natural key that a unique constraint could have caught.
- Teach the shape rather than the answer. A lease cannot distinguish a dead worker from a slow one, so a handler that outruns its lease is running twice by design, and deploys deliver the other half by killing handlers mid-run on every rollout. Both of their candidate theories produce identical duplicate rows, which is why the evidence has to come from timings rather than from argument.
- Hand over a checklist they execute: a natural key on every write the handler performs so the second copy collides rather than appends, the record of intent written before any external effect, a lease heartbeat while running, and the metric that shows it working.
- Keep ownership with them deliberately. Pair on the first write, then step back; if you finish it yourself you have closed one ticket and left the same person stuck on the next redelivery.
Follow-up
- How would you distinguish a genuine double-delivery from a lease expiry using only the data already stored?
- Their handler calls an external endpoint before recording that it did. What do you tell them to change first?
Argue against a design, lose, and commit anyway
Medium · disagreement · service boundaries · decision records
Describe a design you argued against and lost. State the failure you predicted as a named mechanism, not a feeling about complexity: two services that would need one transaction, a projection with no rebuild path, a write path with no idempotency key. Say what evidence you brought, what the decision maker weighed instead, and what you did after the decision was made: what you instrumented, what you wrote down, and whether the prediction came true. Five minutes.
Approach
- State the prediction in falsifiable form up front: the mechanism, the condition that triggers it, and the observable outcome. A prediction that cannot be checked also cannot be credited to you later.
- Show the evidence you had at the time and label each piece honestly as measured, analogous, or intuition. Keeping the intuition is fine; disguising it as data is the thing that erodes your standing in the next argument.
- Represent the opposing case at full strength, including the constraint you did not control: a fixed date, a team boundary, or the fact that the decision was cheap to reverse and yours was not.
- Make disagree-and-commit concrete. Name the artefact you left behind so the prediction could be settled without you: the alert and its threshold, the counter on the dashboard, the decision note that recorded the trade-off and the condition that would revisit it.
Follow-up
- What threshold on that alert would have proved you right, and did anyone ever look at it?
- If the same proposal arrived tomorrow with the same deadline, would you argue it the same way?
Ship under a deadline and bound the debt you chose
Medium · pagination · technical debt · tradeoffs
You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.
Approach
- Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
- Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
- Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
- Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.
Follow-up
- At what page depth does the offset version breach your latency budget, given your page size and row counts?
- What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?