cswork / libraryOpen practice workspace

Interview guides · Affinity solutions

Affinity solutions Software Engineer Interview Guide

Affinity solutions Software Engineer interview guide: reported coding, SQL and query-optimization questions, project deep dives and a 7-day prep plan.

The 60-second brief

Candidates describe Affinity solutions as a data-centric company whose engineers build, run and scale the software platforms that process data for the company's clients. In this role you would write application code, tune database queries, design features with product managers and other engineers, and carry a feature from design through deployment and monitoring. Beyond that, the reports name no products, customers or team structure, so this guide does not guess at them.

This guide is for candidates interviewing for the Software Engineer role at Affinity solutions. It covers the reported question types (medium algorithm problems, optimizing slow code, string and array tasks, multi-table SQL with aggregation, and query optimization on large data), the reported mix of live coding, SQL checks and project deep dives, and the behavioral material that goes with a collaborative engineering role. It does not cover other roles at the company.

Candidates report an initial screening, then several technical rounds that mix live coding, SQL checks and deep dives into past work, with engineers and managers involved. No exact round order or count is reported, so confirm the sequence with your recruiter.

Focus areas

  • Algorithmic Problem Solving
  • SQL
  • Coding Challenges

Overview

The reported requirements for this role are fluency in one major programming language, strong SQL and a working grasp of object-oriented design. Cloud infrastructure, CI/CD pipelines and large-scale data processing appear as extras. That mix shows where to spend preparation time: clean code on medium algorithm problems, joins, aggregation and window functions in SQL, and the habit of stating the cost of whatever you write.

Candidates describe a process that opens with a screening for background and behavioral fit and continues through several technical rounds. Those rounds combine live coding, SQL checks and detailed conversations about projects you have already built. Candidates also report meeting a mix of engineers and managers, and that the number of technical rounds can change with the team. Treat the sequence as unconfirmed until your recruiter gives it to you.

Reported coding difficulty is easy to medium, so the margin is made in execution rather than in obscure tricks. For the algorithm questions, say the brute-force approach, name the data structure you switch to and why, then give time and space complexity for the final version. For the SQL questions, state the grain of the result before writing joins, and be ready to explain how the query behaves when the tables grow.

System design is reported as something that may come up depending on seniority. If you are applying above entry level, rehearse a high-level design out loud: components, the data model, where the database becomes the bottleneck. Day-to-day duties candidates mention (code review, troubleshooting existing processes, owning features after release) are also good material for behavioral answers.

Where to focus your preparation

Candidates report the stages below in general terms only; no fixed order, count or length per stage is reported, so use them as a map of what to practise and confirm the real schedule with your recruiter.

Preparation focus

Candidates report a progression that starts with an initial screening and moves into several technical rounds. Those rounds mix live coding, SQL proficiency checks and deep dives into previous work, and they involve engineers and managers. The number of technical rounds is said to shift with team needs, and a system design discussion may appear for more senior roles. Because no round sequence is pinned down, prepare all three technical formats evenly: solve medium problems aloud in your main language, write multi-table SQL by hand, and rehearse two or three projects from your resume in detail, including the trade-offs you chose.

What to demonstrate

  • Working code for medium algorithm and string or array problems, with time and space complexity stated
  • SQL fluency: joins, aggregation, subqueries, window functions, indexing and schema design
  • How clearly you explain past projects and the trade-offs behind your technical choices
  • Collaboration and communication, which candidates report as part of the screening and later conversations

How to prepare

  • Ask the recruiter which stages involve live coding, which involve SQL, and whether a system design discussion is expected at your level
  • Solve the reported algorithm questions aloud: state the brute force, improve it, then give complexity, including the question about optimizing an inefficient function
  • Write the multi-table join and the query-optimization answer from memory, with an EXPLAIN-based explanation of why an index helps
  • Prepare a short deep dive for two resume projects: the problem, your decision, the alternative you rejected and what you would change

Where candidates lose points

Starting to code a medium algorithm problem before stating a brute-force approach and its complexity

Spend the first moments restating the input, naming the obvious O(n^2) or exhaustive solution, and saying which structure (hash map, two pointers, sort plus scan) removes the cost. Finish by giving time and space for the final code, not just the time.

Answering the question about optimizing an inefficient function with 'I would add caching' or 'I would rewrite it' and no measurement

Describe the sequence: reproduce with a representative input, profile to find the hot spot, check the complexity of the loop or lookup involved, change one thing, and compare before and after. Mention the memory cost of any cache or index you add.

Writing a join-and-aggregate query that inflates totals or silently drops rows

Say the grain of the result first, check which joins are one-to-many before summing, aggregate in a subquery or CTE when needed, use LEFT JOIN where unmatched rows matter, and know when COUNT(DISTINCT ...) is the right count.

Treating query optimization as 'add an index' without reading a plan

Walk through EXPLAIN (ANALYZE): sequential scan versus index scan, estimated versus actual rows, join order and sort cost. Then cover composite index column order, predicates that defeat an index such as a function on the column, and the write cost of extra indexes.

Being unable to explain the reasoning inside a project on your own resume

For each listed project, write the problem, two alternatives you considered, why you chose yours, what broke and what you would do differently. Candidates report that deep dives into past work are part of the technical rounds, so rehearse these aloud.

Questions & practice

How would you approach solving a medium-difficulty algorithmic challenge while explaining your time and space

Medium · Algorithms and Data Structures

How would you approach solving a medium-difficulty algorithmic challenge while explaining your time and space complexity?

Approach

  1. Pin down the constraints before choosing anything: input size, value ranges, sorted or not, duplicates allowed. With n around 10^5, an O(n^2) pass is roughly 10^10 steps, which rules it out and points to O(n log n) or O(n).
  2. Say the brute force and its cost first, then improve it. For pair-sum, for example: nested loops are O(n^2) time and O(1) space; sorting plus two pointers is O(n log n); a hash map of seen values is O(n) time and O(n) space.
  3. Choose the data structure that matches the access pattern: a hash map or set for membership (O(1) average), two pointers or a sliding window over contiguous ranges, a heap of size k for top-k (O(n log k)), BFS with a queue for shortest paths in unweighted graphs.
  4. Give time and space separately and count everything. That includes the recursion stack (O(depth)), any copy made for sorting, and the output itself. Note that hash operations are O(1) on average but can degrade to O(n) when keys collide.
  5. Before calling it done, trace one small input aloud and check the edge cases: empty input, a single element, all duplicates, negative numbers, and integer overflow in languages with fixed-width ints.

Follow-up

  • If you are allowed only O(1) extra space, which version would you pick, and what does it cost in time?
  • Is your hash-map solution O(1) per lookup in the worst case? What input would break that?
  • How would the solution change if the input came as a stream and you could not store all of it?

Describe your process for optimizing an inefficient function or algorithm.

Medium · Algorithms and Data Structures

Approach

  1. Measure before changing anything. Run a profiler (cProfile or py-spy in Python, a JFR or async-profiler capture in Java) on input sized like production and find the hotspot, because the slow part is often not where you expect.
  2. Look for an asymptotic problem first. Common examples: a membership test against a list inside a loop is O(n*m), and switching to a set makes it O(n+m) on average. Repeated string concatenation in a Java loop copies the string each time; StringBuilder or ''.join in Python keeps it linear.
  3. Remove repeated work. Hoist invariant computation out of loops, memoize pure functions with overlapping subproblems, and precompute prefix sums so that each range-sum query costs O(1) after O(n) setup.
  4. If the hot loop calls a database or a remote service, fix the N+1 pattern by batching (one query with IN or a join), then cache only data that changes slowly.
  5. Pin the existing behaviour with tests before refactoring and benchmark before and after on the same input. Name the trade-off you accepted, such as more memory for speed or less readable code, and stop once the target is met.

Follow-up

  • Your fix replaces a list with a set. What happens to memory with 100 million items, and what would you do then?
  • How do you prove the optimized function returns exactly the same results as the original?
  • The profiler shows most of the time is spent waiting on I/O, not on CPU. What changes in your approach?

Can you implement a solution for [common data structure task, e.g., string manipulation or array traversal]?

Medium · Algorithms and Data Structures

Approach

  1. Commit to a concrete version of the task and restate it with an example. For instance: is it an anagram, reverse the words, longest substring without repeating characters, or move zeros to the end of an array. Confirm the character set, case sensitivity and whether the change must happen in place.
  2. For counting problems such as anagrams or the first unique character, use a frequency map (a 26-slot array for lowercase ASCII, a hash map otherwise). That is O(n) time, and O(1) space for a fixed alphabet.
  3. For contiguous-range problems, use a sliding window. In the longest substring without repeats, store each character's last index and move the left edge to max(left, last[c] + 1). This is O(n) time and O(min(n, alphabet)) space.
  4. For in-place array traversal, use a read pointer and a write pointer. In move-zeros, copy each non-zero to the write index and then fill the rest with zeros: O(n) time, O(1) extra space, and the order of the non-zeros is preserved.
  5. Remember that strings are immutable in Python and Java, so build a list or a StringBuilder and join once. Test the empty string, a single character, runs of spaces, and window-boundary off-by-one errors.

Follow-up

  • Can you do it in place with O(1) extra space? What do you give up?
  • How does your solution change if the input is Unicode rather than ASCII?
  • What if the string is too large to fit in memory and arrives in chunks?

Write a SQL query to join multiple tables and aggregate data based on specific criteria.

Medium · Database Management and SQL

Approach

  1. State the output grain first (for example, one row per customer per month), then join outward from the table at that grain. Example shape: customers JOIN orders ON customer_id, then GROUP BY customer and month with SUM(amount).
  2. Choose the join type on purpose. A LEFT JOIN keeps customers with no orders, but a filter on the right-hand table must go in the ON clause. Put it in WHERE and the NULL rows are discarded, which silently turns the query into an inner join. Use COALESCE(SUM(amount), 0) to show zero instead of NULL.
  3. Watch for fan-out. Joining orders to two one-to-many tables (such as items and payments) multiplies rows and inflates the sums. Pre-aggregate each child table in a CTE or subquery to the join key before joining.
  4. Use WHERE to filter rows before grouping and HAVING to filter groups after aggregation. COUNT(*) counts rows; COUNT(col) skips NULLs; COUNT(DISTINCT col) removes duplicates. Every non-aggregated column in SELECT must appear in GROUP BY.
  5. For top-N per group, compute ROW_NUMBER() or RANK() OVER (PARTITION BY group ORDER BY metric DESC) in a CTE and filter on it in the outer query, because window functions cannot be referenced in WHERE.

Follow-up

  • Return the top three customers by spend in each region. How do you handle ties?
  • Customers with no orders in the period must appear with zero. What exactly changes in your query?
  • How would you check that your joins did not duplicate rows before you trust the totals?

How do you approach query optimization when dealing with large datasets?

Medium · Database Management and SQL

Approach

  1. Start from the plan, not from guesses. Run EXPLAIN (ANALYZE, BUFFERS) in PostgreSQL, or EXPLAIN / EXPLAIN ANALYZE in MySQL. Look for sequential scans of large tables, estimated row counts far from the actual ones (stale statistics, so run ANALYZE), nested loops over many rows, and sorts or hashes spilling to disk.
  2. Index the filter and join columns. In a composite B-tree index, put the equality columns before the range column; the index serves queries on its leftmost prefix. A covering index (INCLUDE in PostgreSQL 11+) can allow an index-only scan. Every index also slows writes and takes storage.
  3. Keep predicates sargable. WHERE DATE(created_at) = '2024-01-01', a leading-wildcard LIKE '%abc', or an implicit type cast on the indexed column can stop the index from being used. Rewrite the first as created_at >= '2024-01-01' AND created_at < '2024-01-02'.
  4. Read less data: select only the columns you need, filter before joining, pre-aggregate, and replace large OFFSET paging with keyset pagination (WHERE id > last_id ORDER BY id LIMIT n), because OFFSET still reads and discards every skipped row.
  5. At larger scale, partition by date so filters prune partitions, keep summary tables or materialized views for repeated aggregates (and accept that they go stale between refreshes), and batch large UPDATEs and DELETEs to keep locks and transaction log volume small.

Follow-up

  • The right index exists, but the planner chose a sequential scan. Why might that be the correct choice?
  • How would you remove hundreds of millions of old rows from a live table without blocking writes?
  • When would you add a summary table or denormalize instead of adding another index?

Convert the listing endpoint from offset pages to stable cursors

Medium · pagination · cursors · index access path

GET /v1/resources returns a tenant's resources newest-updated first, today with page and per_page, backed by index (tenant_id, status, updated_at DESC, resource_id DESC). Callers are a browser feed and a nightly sync job that walks every page. Users report items appearing twice or vanishing between pages, and page 400 is slow. Design the cursor contract: what the cursor contains and how it is encoded, the exact WHERE and ORDER BY, what happens when a row's updated_at changes mid-walk, how a client detects the end, and what the sync job does when a cursor is rejected.

Approach

  1. Separate the two defects, because they have different fixes. Cost: OFFSET n makes the engine produce and discard n rows, so price grows with page depth rather than page size and page 400 pays for 400 pages of work. Correctness: while the set shifts, rows cross the offset boundary and are skipped or repeated, and nothing in the response lets the client detect it.
  2. Write the seek: WHERE tenant_id = $1 AND status = $2 AND (updated_at, resource_id) < ($k, $id) ORDER BY updated_at DESC, resource_id DESC LIMIT n. The tie-break is not decoration - updated_at is not unique, and two rows sharing a timestamp across a page boundary reintroduce exactly the skip this was adopted to remove. PostgreSQL seeks the composite index on the row-value comparison directly; on an engine that does not optimise a row constructor, expand it into the equivalent OR form or the plan quietly degrades to a scan.
  3. Encode the cursor as an opaque token carrying the sort key, the id, and a fingerprint of the filter and sort order, signed or at minimum validated. A cursor replayed against a different sort or filter must be a 400 with its own code, not a silently wrong page - the sync job cannot notice the difference otherwise.
  4. State the guarantee precisely rather than generously. Keyset is stable against inserts and deletes elsewhere in the set, because the position is a value and not a count. It is not a snapshot: updated_at is mutable, so a row that is edited during the walk re-sorts and may be seen twice or not at all. If the sync job needs exactly-once coverage, order on an immutable key such as (created_at, resource_id), or walk resource_revision by revision_id and treat updated_at as data.

Follow-up

  • The client wants a total count and the ability to jump to page 400. What can you honestly offer instead, and what does each option cost?
  • How do you paginate backwards, and what does that require of the index?

Design the bulk write endpoint a migration script retries blindly

Medium · batch api · partial failure · idempotency

A customer's migration script pushes 2 million resources through POST /v1/resources:batch, up to 500 items per call, and retries any call that errors or times out. Within a call some items fail validation, some collide with rows that already exist, and some succeed. Specify the request and response shape, whether a batch is atomic or per-item, how idempotency works for the call and for each item, the status code for a mixed outcome, the size and item-count limits with their error codes, and exactly what the script does after a timeout mid-batch.

Approach

  1. Choose atomicity deliberately and price it. All-or-nothing means one transaction holding locks for the whole batch on a primary already absorbing about 1.2k writes/second, which bounds batch size by lock duration, and it turns one bad row into 499 rejections the script must resend. Per-item partial success is the right default for a migration, and the contract's job is then to make a partial outcome impossible to miss.
  2. Require a client-supplied id on every item and echo it in every result. Deriving a per-item key from the array index breaks the first time the script resends a batch with the failures removed: the indices shift, previously-succeeded items acquire new keys, and they are created a second time.
  3. Key the effects at two levels. The call's Idempotency-Key covers an exact resend of the same bytes; per-item keys of (tenant_id, client_item_id) make a partially-applied batch safe to resend whole. Resending an identical batch must reproduce the same per-item results, not 500 conflicts the script has to interpret.
  4. Answer a mixed outcome with one status plus per-item detail: 200, or 207 borrowed from WebDAV if you prefer it - document whichever you pick - carrying an array of client_item_id, per-item status, and either resource_id or an error code from the same taxonomy the single-item endpoint uses. Reserve 4xx for the request as a whole: unparseable body, too many items, payload over the limit (413). Put a failed count at the top level so that even a script checking only the cheapest thing cannot conclude success while rows were dropped.

Follow-up

  • At 500 items per call, what is the wall-clock time for 2 million rows, and what pacing do you publish so the migration does not become an incident?
  • One item in every batch fails with the same code. How does the script discover that without a human reading logs?

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

Medium · lock contention · ddl · migrations

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

Approach

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

Follow-up

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

Suggested practice checklist

Seven days built around the reported question types: algorithm narration, string and array implementation, multi-table SQL, query optimization and project deep dives.

Day 1 — Confirm the format and set up your toolkit

  • Email your recruiter to ask which stages involve live coding, which involve SQL, whether system design is expected at your level and which language you may use
  • Pick the one language you will code in throughout and write a one-page cheat sheet of its hash map, list, string and sorting operations and their costs
  • List the reported topics (complexity analysis, arrays, linked lists, hash maps, subqueries, window functions, indexing, normalization) and mark the two you feel weakest

Deliverable: A recruiter email with your questions, a language cheat sheet and a ranked list of weak topics.

Day 2 — Narrate medium algorithm problems and optimize slow code

  • Solve two medium problems aloud, such as grouping anagrams and merging overlapping intervals, saying brute force, improvement and final time and space complexity each time
  • Take a function with a nested loop that checks for duplicates and rewrite it with a hash set, then explain what you measured or would measure before and after
  • Write a three-sentence template for the process of optimizing an inefficient function, from reproducing the slowness to verifying the fix

Deliverable: Two solved problems with complexity statements and one before-and-after optimization write-up.

Day 3 — String and array implementation drills

  • Implement a string task (reverse words in place, valid palindrome ignoring punctuation) and an array task (two-pointer pair search, prefix sums, sliding window) in your main language
  • For each, list edge cases (empty input, single element, duplicates, negative numbers, non-ASCII text) and run them as tests
  • Refactor one of your solutions for readability: rename variables, extract a helper, remove a clever one-liner you cannot explain

Deliverable: Four working solutions with an edge-case test list and one refactored version.

Day 4 — Multi-table joins and aggregation

  • Using a customers, orders and order_items schema, write a three-table join that totals revenue per customer per month, and state the grain before you write it
  • Write the same result with a subquery and with a window function (running total or rank per customer)
  • Break your own query deliberately with a one-to-many join and show how the sums inflate, then fix it by pre-aggregating

Deliverable: Three runnable queries on one schema plus a note on how you detected and fixed the join fan-out.

Day 5 — Query optimization and schema design

  • Run EXPLAIN ANALYZE on a slow filter-and-join query over a large table, add a composite index, and record how the plan and row counts change
  • Write your spoken answer to how you approach query optimization on large datasets: read the plan, fix predicates, choose index column order, reduce rows early, consider pre-aggregation or partitioning
  • Sketch a normalized schema for a small order system and explain one place you would denormalize and why

Deliverable: A before-and-after plan comparison, a written query-optimization answer and a normalized schema sketch.

Day 6 — Project deep dives and design discussion

  • Write a deep-dive sheet for two resume projects: problem, your role, the alternatives you weighed, the decision, the outcome and what you would change

Deliverable: Two project deep-dive sheets and a spoken high-level design for one practice scenario.

Day 7 — Behavioral rehearsal and full dry run

  • Run one full mock: an algorithm problem with complexity, a multi-table SQL query, the query-optimization answer and one project deep dive
  • Write the two questions you will ask your interviewers about the team's engineering workflow

Deliverable: Three STAR outlines, a completed mock covering coding, SQL and a project, and two questions to ask.

Behavioural preparation

Candidates report that the process starts with a screening aimed at background and behavioral fit, and that later conversations go deep on past projects. The role is described as collaborative, working with product managers and other engineers, so prepare stories about ownership, trade-offs and disagreement. Use STAR (Situation, Task, Action, Result) and keep the Action section about what you personally decided.

  1. Walk me through a project on your resume in detail, including the technical choice you would make differently now.
  2. Describe a time you disagreed with another engineer about an approach. How did you handle it, and what was the outcome?
  3. Tell me about a time you made a technical trade-off between speed of delivery and quality.
  4. Describe working with a product manager to turn an ambiguous request into concrete technical requirements.
  5. Tell me about a time you found and fixed a slow query or a slow piece of code. How did you find the cause and how did you confirm the fix?
  6. Describe a feature you owned from design through release and monitoring. What did you check after it shipped?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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?

Turn a code review disagreement into a decision

Easy · code review · optimistic concurrency · communication

A colleague's change updates a row with UPDATE resource SET version = version + 1 WHERE resource_id = $1 AND version = $2 and treats an affected-row count of zero as a successful no-op. You read that as a silently lost update; they think returning 200 is friendlier to clients than returning a conflict. Describe how you have handled a review disagreement of this shape: what goes in the comment, when you leave the thread, and who decides. Then write the comment you would leave here, in under 80 words.

Approach

  1. Sort the disagreement before writing anything. A silently discarded write is a correctness claim about data; the choice between 409 and 412 is taste. Only the first justifies blocking a merge, and saying which one you are doing is most of the value of the comment.
  2. Make the claim reproducible in the comment itself with an interleaving rather than a principle: A reads version 7, B reads version 7, B commits version 8, A's predicate matches zero rows, A is told it succeeded and A's edit is gone.
  3. Offer the alternative with its cost attached: return 409 carrying the current version and the revision that won, so the client can re-read and re-apply. Note that automatic retry is not the fix, because a retry re-reads the winner's state and reapplies an intent formed against data that no longer exists.
  4. Apply an escalation rule you can state: two round trips on the thread, then a call, and the service's owner decides rather than the reviewer. A reviewer who cannot be overruled is a bottleneck with extra steps.

Follow-up

  • Where would you put the test that fails if someone reintroduces the swallowed zero rowcount?
  • The author says clients cannot handle a 409. How do you check whether that is true?