cswork / libraryOpen practice workspace

Interview guides · betterworks

betterworks Software Engineer Interview Guide

betterworks Software Engineer interview guide: reported rounds, coding, debugging, PostgreSQL and architecture questions, plus a seven-day prep plan.

The 60-second brief

betterworks builds a platform for enterprise performance management. Organizations use it to set goals, follow progress against them and keep employees aligned. Candidates describe the engineering work as maintaining and extending that platform while a large monolithic codebase is moved toward a more scalable design, and the reported requirements mention a possible shift from older frameworks to modern JavaScript. Backend work is described in Python, with PostgreSQL as the database that comes up in questions. A Software Engineer there is described as building product features with product teams, joining code reviews and architecture conversations, and helping move legacy services onto newer foundations.

This guide is for engineers preparing for the betterworks Software Engineer loop. It covers the four reported stages, the reported coding, debugging, architecture and PostgreSQL questions, how to handle a pair-programming session in a codebase you did not write, and a seven-day plan. It assumes a backend language such as Python and tells you where JavaScript fluency matters.

Candidates report four stages over roughly three to five weeks: a conversation with a Hiring Manager, Technical Assessment Rounds, Architectural Discussions and Pair-Programming Sessions.

Focus areas

  • System Architecture
  • JavaScript
  • Python

Overview

The work candidates describe sits between new feature delivery and modernization. Product requirements have to become technical specifications, a monolith has to keep running while parts of it are modernized, and the code has to stay fast as usage grows. That mix shapes the loop: the reported questions are practical rather than puzzle-driven. You write a tree algorithm, untangle nested conditionals, repair a failing test suite, explain a system you built and talk through PostgreSQL tuning.

The question list splits into three groups. The first is classic data-structure coding: finding the first and last position of a duplicate value, evaluating an expression with a stack or pointers, finding the lowest common ancestor in a tree where nodes carry parent links, matching a string against a pattern, and compressing a string. The second is working with existing code: simplifying deeply nested if/else logic, getting a broken test suite to pass, and fixing or implementing game logic such as a snake game in JavaScript. The third is architecture: describing a system you designed or worked on, and listing concrete ways to speed up PostgreSQL queries.

Because the stack is reported as Python on the backend with JavaScript on the way in, prepare to read and write both. Candidates are told to be comfortable in JavaScript even if another language is their main one, so do at least some of your practice there, particularly the debugging and game-logic work. For the architecture material, the reported tip is that diagrams from your past roles get revisited in later stages, so pick systems you can redraw the same way twice and explain end to end.

Preparation therefore has two halves. Build fluency on the coding and debugging patterns above until you can narrate them while typing. Then prepare two system stories with data flow, scaling limits and database choices, plus a short, measurable list of PostgreSQL techniques. The plan below spreads these across seven days and ends with a full rehearsal of the four stages.

The betterworks interview process

Candidates describe four stages. The descriptions below cover what each is reported to contain and how to prepare for that format; no question is tied to a particular stage unless candidates say so.

Conversation with Hiring Manager

Candidates report that the process opens with a discussion with a Hiring Manager or VP about fit for the role. Treat it as a conversation about your background and judgment, not a coding screen. Be ready to summarize your backend experience, your database exposure and any time you worked on a large legacy codebase, since modernization is part of the role as described.

What to demonstrate

  • Fit for the role, as the stage is described
  • How clearly you describe your own projects and the decisions you owned
  • Relevant background in backend work, databases and system architecture

How to prepare

  • Choose two projects and write down, for each, the decision you owned, the alternatives you rejected and what the decision cost.
  • Prepare a short account of your Python or other backend experience and any JavaScript you have written, so the language question has an honest, concrete answer.
  • Pick which system you will diagram later and rehearse its explanation now, since architecture material is reported to come back in later stages.
  • Write down questions about how the team is splitting up the monolith and how engineers work with product teams.

Technical Assessment Rounds

Candidates report several technical assessments mixing coding challenges and debugging tasks. The coding questions reported for this company are list and position finding, expression evaluation with a stack or pointers, a tree with parent links, pattern matching and string compression; the debugging questions are nested conditionals, a broken test suite and JavaScript game logic. Work through that whole set aloud, because explaining the trade-offs of an approach is part of what candidates are told to do.

What to demonstrate

  • Working, readable code for standard data-structure problems
  • A clear account of time and space trade-offs between alternative approaches
  • A systematic way of breaking down tangled or failing code

How to prepare

  • Solve the parent-link lowest-common-ancestor, the pattern-match and the string-compression problems in Python without notes, then restate each in JavaScript.
  • Practise the duplicate first/last position problem for both an unsorted list and a sorted one, and say which complexity each allows.
  • Take a function with four or five nested branches and refactor it with guard clauses or a lookup table while keeping behaviour identical.
  • Run a failing test suite and write down, for each failure, whether the test or the code is wrong before you change anything.

Architectural Discussions

Candidates report deep-dive discussions of architecture from their own past projects, with PostgreSQL performance as a reported topic. The reported tip is that diagrams from earlier roles may be revisited in later stages, so the picture you draw has to be one you can reproduce consistently. Expect to defend why you chose particular databases or patterns, and how you handled scaling, data flow and bottlenecks.

What to demonstrate

  • Depth of understanding of a system you actually built
  • The reasoning behind database and pattern choices
  • Knowledge of PostgreSQL performance techniques

How to prepare

  • Draw one system from your own work: clients, services, data stores, queues and the request path, plus the limit you hit when load grew.
  • Write the reasons behind each datastore choice and one thing you would change with hindsight.
  • Prepare a PostgreSQL tuning sequence: read the plan with EXPLAIN (ANALYZE, BUFFERS), compare estimated and actual rows, add or reshape an index, rewrite the query, check statistics and vacuum health.
  • Rehearse how a monolith can be split one seam at a time, because the platform is reported to be moving away from one.

Pair-Programming Sessions

Candidates report collaborative sessions in which you fix real code alongside engineers. The format is closer to a working session than a puzzle: you read unfamiliar code, run the tests, change it and talk while you do. Candidates are told that openness to suggestions and the ability to work in an unfamiliar codebase matter here, and JavaScript is part of the reported mix.

What to demonstrate

  • Working code in an existing codebase, produced while collaborating
  • How you respond to suggestions and changes of direction
  • Communicating your reasoning while you type

How to prepare

  • Rehearse the first five minutes in an unknown repository: find the entry point, run the tests, read the failing one, then form a hypothesis out loud.
  • Build a small snake game in JavaScript: a grid, a direction queue, movement, growth, and wall and self-collision checks.
  • Practise fixing someone else's broken tests with a partner, taking a suggestion and adjusting your plan without defending the old one.
  • Keep a short list of JavaScript basics you must be able to write fluently: array methods, closures, equality, the event loop and timers.

Where candidates lose points

Starting the lowest-common-ancestor problem without asking about its inputs

Clarify whether both nodes are guaranteed to be in the tree, whether one can be an ancestor of the other, and whether values are unique. With parent links you can climb from both nodes, either by recording visited ancestors in a set or by equalizing depths and walking up together; say which you chose and why.

Making a failing suite green by editing assertions or deleting tests

For each failure, decide whether the test, the code or the fixture is wrong. Read the assertion, reproduce it in isolation, fix one failure at a time and re-run the whole suite after each change. Say aloud which side you believe is at fault and why.

Going quiet or defensive in a pair-programming session

Narrate what you are reading and what you expect to happen. When your partner suggests another route, try it or explain a specific concern. Candidates are told the session tests how you fit into a team.

Giving a PostgreSQL answer that is only a list of index names

Open with the plan: run EXPLAIN (ANALYZE, BUFFERS), compare estimated and actual rows, look for sequential scans and sorts that spill. Then tie each remedy to the symptom: a composite index, a partial index, a rewrite that removes a function on an indexed column, fresh statistics, or paging by key instead of offset.

An architecture walkthrough that changes between tellings or cannot justify choices

Fix one diagram and one narrative: the request path, the data stores and why each was chosen, the bottleneck you met and what you did about it. Redraw it from memory on separate days until the boxes, arrows and reasons come out the same each time.

Questions & practice

Find the first and last occurrence position of a given duplicate number in a list.

Medium · Technical & Algorithmic Proficiency

Approach

  1. Ask first whether the list is sorted. If it is, run two binary searches: a lower bound for the first index whose value is >= target, and an upper bound for the first index > target, minus one. That is O(log n) time and O(1) space; in Python, bisect_left and bisect_right do this.
  2. After the lower-bound search, confirm the index is inside the list and holds the target before using it. Otherwise return [-1, -1], so a missing value never yields a wrong range.
  3. If the list is unsorted, a single left-to-right pass is optimal at O(n). Record the index the first time the value appears and overwrite last on every match. Sorting first costs O(n log n) and loses the original positions.
  4. Edge cases to state: empty list, one element, every element equal to the target, target at either end. In fixed-width languages, compute mid as lo + (hi - lo) / 2 so it cannot overflow.

Follow-up

  • How would you return the number of occurrences, and what does that cost on a sorted list?
  • If thousands of lookups hit the same unsorted list, what would you precompute so each lookup is O(1)?
  • How do the searches change if the sorted list has been rotated at an unknown pivot?

Solve an expression using stack-based or pointer-based logic.

Medium · Technical & Algorithmic Proficiency

Approach

  1. Pin the grammar before coding: which operators, whether parentheses and unary minus are allowed, multi-digit numbers and whitespace, and whether division truncates toward zero. Python's // floors, so -7 // 2 is -4, not -3.
  2. Stack version for + - * / with no parentheses: scan with an index, build the current number, and when an operator or the end arrives, apply the previous operator. Push for +, push the negation for -, and pop and combine for * and /. Sum the stack at the end. O(n) time, O(n) space.
  3. Pointer version (recursive descent): expr = term (('+' | '-') term), term = factor (('' | '/') factor)*, factor = number | '(' expr ')' | '-' factor. One shared index advances through the string, so precedence and parentheses come from the call structure.
  4. Keep left associativity: 8 - 3 - 2 must give 3, not 7, which is a common bug in naive right-recursive parsers. Test division by zero and malformed input such as '3 +' or unbalanced parentheses, and decide whether each raises an error or returns a sentinel.

Follow-up

  • How would you add exponentiation, which is right-associative and binds tighter than multiplication?
  • How would you convert the expression to Reverse Polish Notation with the shunting-yard algorithm, and why is that convenient?
  • What changes if the expression can contain variables looked up in a map?

Find the least common ancestor for a given tree where each node has an integer value and a link to the parent.

Medium · Technical & Algorithmic Proficiency

Approach

  1. Compare nodes by identity, not by their integer value. Nothing says the values are unique, so two different nodes can hold the same number.
  2. Hash-set approach: walk from the first node to the root and add every ancestor, including the node itself, to a set. Then walk up from the second node until you reach a member of the set. O(h) time and O(h) space, where h is the tree height.
  3. Constant-space approach: compute both depths by walking to the root, lift the deeper node by the difference, then step both up together until they meet. Still O(h) time, but O(1) extra space.
  4. Equivalent two-pointer trick, as in linked-list intersection: advance a and b, and when one hits null, restart it at the other start node. They meet at the ancestor after at most depth(p) + depth(q) + 2 steps, or both reach null if the nodes are in different trees.
  5. Edge cases: one node is an ancestor of the other (the answer is that node), both inputs are the same node, either input is null, or the nodes have no common root.

Follow-up

  • Without parent pointers, how would you find the ancestor from the root, and what does that cost?
  • If the tree is a binary search tree, how can the values alone guide the search?
  • If you must answer many ancestor queries on a fixed tree, what would you precompute (binary lifting, Euler tour), and at what cost?

Given a pattern and a string, determine if the string follows that pattern.

Medium · Technical & Algorithmic Proficiency

Approach

  1. Split the string into words and return false right away if the word count differs from the pattern length.
  2. Check a bijection with two hash maps, pattern character to word and word to pattern character. On each pair, a conflict in either map means false. A single map misses 'abba' against 'dog dog dog dog', where 'a' and 'b' both map to 'dog'.
  3. Time is O(total characters) for the split and the hashing, and space is O(distinct letters + distinct words). In Python, an equivalent check is len(set(pattern)) == len(set(words)) == len(set(zip(pattern, words))) once the lengths match.
  4. Edge cases: empty pattern or empty string, repeated or leading spaces (str.split() collapses them, str.split(' ') does not), and case sensitivity. Ask which behaviour is wanted.

Follow-up

  • If the string has no spaces and each pattern letter can match any non-empty substring, how would you solve it, and what is the worst-case cost?
  • How does this relate to checking whether two strings are isomorphic?
  • Would your answer change if the pattern could contain Unicode characters or the words could be very long?

Implement a string compression algorithm.

Medium · Technical & Algorithmic Proficiency

Approach

  1. Clarify which variant is wanted. One is classic run-length encoding ('aabcccccaaa' becomes 'a2b1c5a3', returning the original if the result is not shorter). The other is the in-place array version, where single characters get no count and counts of 10 or more are written as several digit characters.
  2. Use two pointers: a read pointer scans each run of equal characters, and a write pointer emits the character and then its count. That is O(n) time with O(1) extra space in place. In a new-string version, append parts to a list and join once, rather than concatenating immutable strings in a loop.
  3. Handle the final run after the loop ends. Forgetting to flush it is the most common bug.
  4. Edge cases: empty input, a single character, no repeats at all (RLE doubles the length), and input that already contains digits, which makes the encoded form ambiguous to decode unless counts are delimited or escaped.

Follow-up

  • Write the decoder and show that decode(encode(s)) == s for strings containing digits.
  • When does run-length encoding make data larger, and what would you use for general text instead (Huffman coding, LZ77-family)?
  • How would you compress a stream too large to hold in memory?

Describe specific strategies for optimizing PostgreSQL query performance.

Medium · System Architecture & Optimization

Approach

  1. Measure before tuning. Use pg_stat_statements to find the queries with the highest total time, then EXPLAIN (ANALYZE, BUFFERS) on each one to see the real plan: sequential versus index scans, actual versus estimated rows, and sorts or hashes spilling to disk.
  2. Index for the query's shape. Use composite B-tree indexes with equality columns before range columns, partial indexes for hot subsets, INCLUDE columns for index-only scans (which also need a current visibility map, so vacuum matters), expression indexes such as lower(email), and GIN for jsonb or full-text search.
  3. Keep predicates sargable. A function applied to an indexed column, a leading-wildcard LIKE '%x', or an implicit type cast usually stops a plain index from being used. Fix the ORM's N+1 queries, select only the columns you need, and replace deep OFFSET paging with keyset pagination.
  4. Fix bad estimates and maintenance. Run ANALYZE, raise the statistics target or add extended statistics (CREATE STATISTICS) for correlated columns, check that autovacuum keeps up with bloat, and size work_mem for large sorts. Use a connection pooler such as PgBouncer when connection counts are the limit.
  5. State the trade-offs: every index slows writes and takes space. Note that NOT IN against a subquery that returns a NULL yields no rows, so NOT EXISTS is the safer anti-join.

Follow-up

  • An index exists but the planner ignores it. What are the likely reasons and how do you confirm each one?
  • How do you add an index to a large, busy table without blocking writes, and what is the catch with CREATE INDEX CONCURRENTLY?
  • When would you reach for table partitioning, and what does it cost?

Replace offset paging on the resource feed with keyset

Easy · keyset pagination · composite index · sort order

resource holds resource_id, tenant_id, owner_user_id, title, body_ref, version, status ('draft','active','archived','deleted'), created_at, updated_at, deleted_at, with an index on (tenant_id, status, updated_at DESC, resource_id DESC). The listing endpoint returns active resources for one tenant, newest update first, 50 per page, today with LIMIT 50 OFFSET n. Tenants reach page 400 and rows are created while they read. Write the keyset query, define what the cursor carries and how it is encoded, and say which part of the index each predicate uses. Assume PostgreSQL 16.

Approach

  1. Name the two failures separately. OFFSET 20000 makes the server produce and discard 20,000 rows, so page cost grows with depth rather than with page size. Independently, any write that changes how many rows sort above the offset moves the window between two fetches, and the direction decides which anomaly you get: an insert lands at the head of updated_at DESC and pushes already-returned rows down past the boundary, so they are returned a second time; a delete above the offset, or a row whose updated_at is bumped above the cursor, pulls rows up and one is never returned at all. Nothing in the response reveals either.
  2. Write the seek: WHERE tenant_id = $1 AND status = 'active' AND (updated_at, resource_id) < ($2, $3) ORDER BY updated_at DESC, resource_id DESC LIMIT 50. The row-value comparison is one index range rather than a disjunction, and both columns are NOT NULL, which is what makes that comparison well defined.
  3. Map each predicate onto the index: tenant_id and status are equality on the leading columns, (updated_at, resource_id) is the range, and the ORDER BY matches the index order so no Sort node appears and the scan stops after 50 rows. The DESC in the definition only matters for mixed directions — a plain ascending btree on the same columns is read backwards for this query.
  4. Put both sort columns in the cursor and nothing the client can tamper with into another tenant: base64 of (updated_at, resource_id), validated server-side, with tenant_id taken from the principal.

Follow-up

  • The client asks for 'jump to page 400'. What do you offer instead, and what does the honest version cost?
  • Sort order becomes user-selectable across four columns. How many indexes is that, and which would you refuse to add?

Explain the architecture of a system you have previously designed or worked on.

Medium · System Architecture & Optimization

Approach

  1. Open with context in a few sentences: who used the system, the load you actually measured (requests, data size, tenants), and the problem it solved. Then draw the components and trace one request end to end through client, API, services, database, cache and queues.
  2. Separate your own decisions from the team's. Use 'I' for what you designed or changed, and give each key choice with the alternative you rejected and why: the database, sync versus async processing, the service boundaries.
  3. Describe one bottleneck or incident: what the metric showed, how you found the cause, what you changed and the measured result. That gives scaling and performance claims something concrete behind them.
  4. If the system involved splitting or modernising a monolith, which candidates describe as part of this role, explain the migration path: strangler-style routing, dual writes or backfills, and how you kept both versions consistent during cutover.
  5. Keep the diagram identical from round to round. Candidates report architecture from earlier stages being revisited later, so rehearse the same boxes, names and numbers.

Follow-up

  • What breaks first if traffic grows tenfold, and what would you change?
  • Why that database, and what would have pushed you to a different one?
  • If you rebuilt this system today, what would you do differently?

Shard by tenant when one tenant outgrows a single shard

Hard · sharding · hot partition · routing

One primary holds resource, resource_revision, outbox_event and idempotency_key for every tenant and is at its write ceiling at 1.2k writes/second. tenant_id leads every index. Shard across eight primaries. One tenant holds 22% of all rows and by itself exceeds a single shard's write capacity. Design the routing, the split of that tenant, and the online move of a tenant between shards with writes continuing. State what breaks for queries that are tenant-scoped today, and exactly what a write must do when it arrives at the old shard after the cutover.

Approach

  1. Route on a unit smaller than a tenant from the start. Make the routing key (tenant_id, bucket) with a fixed bucket count - 64 over eight shards - and keep a directory mapping each (tenant_id, bucket) to a shard, carrying a version and cached in every service. An ordinary tenant has all 64 buckets pointing at one shard and behaves exactly as it does today; only the hot tenant has its buckets spread. Hashing tenant_id alone spreads tenants evenly, gives you no way to move one, and has no answer at all for a tenant larger than a node. The bucket count is the part you cannot change later without rehashing rows, so pick it well above the shard count and rebalance by moving buckets, not by re-bucketing.
  2. For the tenant that exceeds one node, its buckets must land on different shards - that is the whole point of bucketing it, and buckets confined to its own shard would rename the rows while leaving every write on the node whose ceiling it already exceeds. Size it from measured numbers rather than from its row share: 22% of rows says nothing about write rate. The current primary tops out near 1.2k writes/second on this hardware and workload, so a tenant peaking at W writes/second needs its buckets spread over at least ceil(W / headroom-per-shard) shards, where the divisor is the share of each shard's ceiling you are willing to give it while that shard still serves other tenants - not the full 1.2k. Size on its peak, not its mean.
  3. Fix the co-location invariant at the right grain. What must commit in one transaction is a resource, its resource_revision row and its outbox_event row, so the bucket is a property of the resource: derive it once at creation and stamp it into resource_id, and every later revision and event routes with its parent for free. Per-tenant co-location was never the requirement, and mistaking it for one is what makes a tenant look unsplittable. What genuinely breaks is an invariant spanning two resources of one tenant - a per-tenant counter, uniqueness across its resources - which now needs either a home-shard table or two-phase commit, and 2PC at this write rate is not a serious option.
  4. Keep the idempotency constraint arbitrating, because it is now enforced per shard. PRIMARY KEY (tenant_id, idempotency_key) only continues to reject a retry if the same key always lands on the same shard, so derive the create-path bucket from hash(tenant_id, idempotency_key) and mint the new resource_id inside that bucket, which also puts the key row and the resource it guards in one transaction. A bucket chosen from anything that differs between a request and its retry - a timestamp, the worker id, a client-supplied resource id - splits one key across two shards, both inserts succeed, and the write endpoint's retry safety is silently gone.

Follow-up

  • The fence lasts 90 seconds instead of 4 because the final delta keeps growing. What is happening, and what do you do while the tenant is fenced?
  • Two tenants must merge into one account. What does that cost under this scheme, and which step is not reversible?

Simplify a complex function containing deeply nested if/else statements.

Medium · Debugging & Practical Implementation

Approach

  1. Lock in current behaviour before changing anything: write characterisation tests that cover every branch, built from a truth table of the conditions, so the refactor is checked against the old outputs, including the odd ones.
  2. Flatten with guard clauses: handle invalid or terminal cases first with early returns, and invert conditions using De Morgan's laws (not (a and b) is (not a) or (not b)) so the main path is no longer nested.
  3. Extract each compound condition into a well-named predicate function. Where branches choose behaviour by a type or status value, replace the chain with a lookup table (dict or Map dispatch) or with polymorphism.
  4. Watch what can silently change: evaluation order and short-circuiting when conditions have side effects, overlapping conditions where the first match currently wins, and unreachable branches. Point those out instead of deleting them quietly.
  5. Explain the result in terms of fewer paths: cyclomatic complexity is decision points + 1, so show the count before and after.

Follow-up

  • How do you prove the refactored function behaves identically, and what does branch coverage tell you that line coverage does not?
  • When is a lookup table or polymorphism worse than a plain if/else?
  • How would you split this change into small, reviewable commits?

Debug a provided test suite with broken tests to resolve failures and ensure all tests pass.

Medium · Debugging & Practical Implementation

Approach

  1. Run the whole suite once and list every failure before touching code. Group them by shared cause: one broken import, fixture or setup hook can fail dozens of tests, so fix the earliest root cause first.
  2. For each failure, decide whether the test or the code is wrong. Read the test name and its assertion to recover the intended behaviour, then compare expected against actual. Never edit an assertion just to match current output, and never skip the test.
  3. Isolate a failure by running that single test. If it passes alone but fails in the full run, suspect shared state: module-level variables, mocks or spies not reset between tests, database rows left behind, or order dependence.
  4. Check the usual JavaScript and Python traps: an async test that does not await or return its promise, fake timers left installed, and dependence on the current date, timezone or random seeds.
  5. Fix one cause, rerun the full suite, and repeat. Finish with a clean full run, not a run of only the tests you touched.

Follow-up

  • A test passes locally and fails in CI. What do you check first?
  • How would you find which earlier test pollutes the state a failing test depends on?
  • Once the suite is green, what would you add so the same breakage is caught earlier next time?

Fix or implement game logic (e.g., snake game) in JavaScript.

Medium · Debugging & Practical Implementation

Approach

  1. Model the state explicitly: the snake as an array of {x, y} cells with the head first, the current direction, a queued next direction, the food cell, the grid size, score and an alive flag. Keep the update a pure function, state in and state out, separate from rendering, so it can be tested without a browser.
  2. Each tick: compute the new head from the direction, end the game on a wall hit, then check self-collision. Moving into the cell the tail is leaving is legal unless the snake is growing this tick. If the head lands on food, keep the tail and respawn the food; otherwise pop the tail.
  3. Input handling: ignore a key that reverses the current direction, and apply at most one direction change per tick. Two fast key presses inside one tick can otherwise turn the snake back into itself.
  4. Performance and correctness: self-collision is O(length) per tick with an array, or O(1) with a Set of 'x,y' keys kept in sync. Spawn food by picking at random from the free cells, not by retrying random cells, which stalls as the board fills.
  5. Game loop: run setInterval or requestAnimationFrame with a time accumulator, and clear the interval or cancel the frame on game over so a restart does not stack two loops.

Follow-up

  • How would you add wrap-around walls or speed that increases with score?
  • How would you write unit tests for the collision and growth rules?
  • What changes if the board is rendered on a canvas instead of DOM cells?

Suggested practice checklist

The plan follows the four reported stages: conversation, coding and debugging, architecture, and pair programming. Each day ends with something you can show, written or spoken, and it points at the reported questions it practises.

Day 1 — Map the loop and pick your systems

  • Write the four reported stages in order and note, for each, whether you will be talking, coding alone or coding with someone.
  • Choose two past projects for the Hiring Manager conversation and write the decision, alternatives and cost for each.
  • Pick the one system you will diagram for the architecture discussions and sketch its first version from memory.

Deliverable: A one-page loop map, two project summaries with decisions and costs, and a first system sketch.

Day 2 — Lists, positions and strings

  • Solve the first/last position of a duplicate value for an unsorted list in one pass, then for a sorted list with two binary searches, and write both complexities.
  • Solve the pattern-and-string matching problem with a two-way mapping and cover the edge cases of unequal lengths and repeated words.
  • Implement string compression, first allocating a new buffer and then in place with a read and a write pointer, and test empty and single-character input.

Deliverable: Three solutions in Python with edge-case tests, plus a spoken one-minute explanation of each.

Day 3 — Stacks, pointers and trees with parent links

  • Evaluate an arithmetic expression with a stack, handling precedence and parentheses, then redo it with a pointer-based scan.
  • Find the lowest common ancestor in a tree with parent links in two ways: an ancestor set, and depth equalization with a joint climb.
  • Write down the time and space cost of each variant and the case in which each one wins.

Deliverable: Working expression and ancestor solutions plus a table comparing their time and space costs.

Day 4 — Debugging: nested logic and broken tests

  • Take a function with deeply nested if/else, write characterization tests first, then flatten it with early returns or a lookup table.
  • Break three tests in a small project in different ways: a wrong assertion, a stale fixture and a real bug. Fix each after deciding which side is wrong.
  • Do the same refactor in JavaScript and note the language differences you tripped over.

Deliverable: A before-and-after refactor with passing tests and a written failure log naming the root cause of each broken test.

Day 5 — JavaScript game logic and a pair-programming rehearsal

  • Implement snake in JavaScript: grid state, direction handling, movement, growth, food placement and collision checks.
  • Introduce two bugs, such as reversing into yourself and food spawning on the body, hand it to a friend and fix them together while narrating.
  • Review how you took suggestions and write down two things to change.

Deliverable: A working snake implementation and a short written review of the paired session.

Day 6 — Architecture and PostgreSQL

  • Redraw your chosen system from memory and rehearse its explanation with data flow, scaling limits and the reasons for each datastore.
  • Take a slow query on a table you can create locally and tune it step by step using EXPLAIN (ANALYZE, BUFFERS), recording the plan before and after.
  • Work the keyset-paging exercise and the tenant-sharding exercise from the practice library to extend your PostgreSQL and scaling vocabulary.

Deliverable: A final system diagram with a spoken walkthrough, plus a tuning log showing plan changes and timings.

Day 7 — Full rehearsal and behavioral stories

  • Run all four stages as a mock: a short conversation about your projects, one coding problem, one debugging task and an architecture walkthrough.
  • Practise the estimation, breaking-change and reversal stories from the practice library so they are ready for any follow-up.
  • Write a list of the points where you lost your thread and rehearse those parts again.

Deliverable: A recorded or written mock run with a list of weak spots and a revised plan for them.

Stories for conversation and pairing rounds

The conversation stages and the collaborative sessions are where candidates describe behavior-based discussion. Expect to talk about decisions you owned, how you work in code you did not write, how you take feedback, and how you handle moving older systems forward. Prepare stories where you can name the situation, your own contribution, the trade-off and the result.

  1. Explain the architecture of a system you have previously designed or worked on, and name one decision you would make differently now.
  2. Tell me about a time you had to understand and change a codebase you did not write. How did you find your way in?
  3. Describe a time a colleague suggested a different approach during pairing or code review. What did you do with it?
  4. Tell me about moving a legacy component toward a newer design. How did you pick what to move first, and how did you keep the system working?
  5. Describe a time you had to work in a language or framework you did not know well. How did you ramp up and what did you ship?
  6. Tell me about a requirement that was unclear. How did you pin it down before building?

Estimate work you have never done and defend the range

Hard · estimation · backfills · expand-contract

You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.

Approach

  1. Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
  2. Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
  3. Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
  4. Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.

Follow-up

  • How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
  • Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?

Tell callers you do not own that their integration breaks

Medium · deprecation · compatibility · stakeholders

A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.

Approach

  1. Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
  2. Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
  3. Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
  4. Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.

Follow-up

  • How would you detect a consumer that reads the field only during a monthly export?
  • One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?

Reverse your own decision and price the reversal

Medium · reversibility · measurement · migrations

Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.

Approach

  1. State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
  2. Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
  3. Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
  4. Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.

Follow-up

  • What in that decision was irreversible, and did you know it was irreversible when you made it?
  • How did you tell the people who had already built on top of the original decision?