cswork / libraryOpen practice workspace

Interview guides · anecdotes

anecdotes Software Engineer Interview Guide

anecdotes Software Engineer interview guide: reported system design questions, the home assignment, and a seven-day plan for the loop.

The 60-second brief

anecdotes makes compliance automation software for enterprises. The role description lists backend work on integrations and storage, plus code reviews and working with developers and product managers on feature requirements. Candidates report that the interview day includes technical discussions with leadership, where you are expected to say why a technical path suits the business.

This guide is for backend-leaning Software Engineer candidates preparing for the anecdotes loop. It covers the four reported design questions, the home assignment candidates describe, and the stack the role lists: a modern backend language (Python, Node.js and Go are the examples given), relational databases and REST API design, with cloud (AWS or GCP) and containers (Docker, Kubernetes) as extras.

Candidates report an HR screen, then a technical assessment that is often a home assignment, then an interview day covering system design, behavioral alignment and technical discussions with leadership. Reports put the whole process at two to three weeks, with many candidates hearing a decision within two.

Focus areas

  • System Design
  • API Design (REST endpoints)
  • Database Schema Design

Overview

The reported work sits where data comes in, gets stored with history, and triggers downstream actions: ingestion, API integrations, versioned records and event-driven processing. The four reported design questions follow the same shape. One asks for a document management application with version control, API endpoints and a database schema. One asks how to retrieve version 50 versus the latest. One asks how to separate metadata storage from raw data in a high-volume system. One asks what changes when an email-sending system grows from 10 users to 100,000.

Because those questions share a domain, you can prepare them from one reference design. Build a versioned document service: an append-only version table, a current-version pointer, an object store for file content, a REST surface for create, read, list and update, and a queue-backed notifier that emails users when a version changes. Then practise carving each question out of it. Scenarios candidates report from the same area are concurrent edits to the same document version and a sudden spike in load, so rehearse the concurrency control and the backpressure story as well as the happy path.

Candidates report a home assignment after the HR screen, with fetching data from an API as the example task, and that edge cases, error handling and documentation from that submission come up in later conversations. The reports add that you should justify every technology choice with its trade-offs, be ready to say how you use AI tools in your own coding, and keep a few projects you can walk through in depth. The plan below splits time between the home assignment skills, the four design questions and the communication work.

Where to focus your preparation

Candidates describe the process as a whole rather than as a numbered list of rounds, so this guide treats it as one preparation focus that spans the screen, the technical assessment and the interview day.

Preparation focus

Candidates report that the process starts with an HR screen and moves to a technical assessment, often a home assignment such as fetching data from an API, before an interview day with system design, behavioral alignment and technical discussions with leadership. Because no per-round format is reported, prepare for the three kinds of work separately and confirm the details with your recruiter. For the assignment, ship small and complete code: pagination, timeouts, retries, error handling, tests and a README. For system design, practise talking through a design aloud, naming the alternative you rejected each time. For the leadership conversations, prepare to explain why a technical path suits the business, and keep two or three past projects ready to walk through.

What to demonstrate

  • Reported areas of focus: a small integration task with error handling and edge cases, which candidates say are discussed afterwards
  • Reported areas of focus: versioned storage, REST endpoints, schemas and growth from 10 to 100,000 recipients
  • Reported areas of focus: choice of data stores and patterns defended with trade-offs, such as SQL versus NoSQL for metadata versus raw data and queues for background work
  • Reported areas of focus: explaining technical decisions to team leads and leadership

How to prepare

  • Build a small paginated API client in Python, Node.js or Go with timeouts, backoff on retryable errors, response validation, tests and a README that lists assumptions
  • Write a full answer to the document versioning question: tables, endpoints with status codes, how a conflicting edit is rejected, and where file content lives
  • Draw the email scale-up as a before and after diagram: request path, queue, workers, provider limits, retries, per-recipient status
  • Pick two or three past projects and write the problem, constraints, your decision, and the result for each; add a short account of how you use AI coding tools and check their output

Where candidates lose points

Overwriting the document row on each edit, so there is no history to serve when the question asks for version 50

Model versions as append-only rows keyed by (document_id, version_number), with the parent row holding a pointer to the current version. State whether each version stores a full snapshot or a delta, and what reconstructing version 50 costs under each choice.

Letting concurrent edits silently overwrite each other, then having no answer when asked how data stays consistent when several users edit the same version

Use optimistic concurrency: the client sends the version it edited from (an ETag or version number), the server inserts the next version under a unique constraint, and a stale writer gets a 409 or 412 with the current version so it can merge or retry.

Answering the 10 to 100,000 email question with only 'add more servers' while the send still happens inside the request

Move sending behind a queue with background workers. Cover batching, an idempotency key per message and recipient, provider rate limits, retries with backoff, a dead-letter queue, and a per-recipient status record, and name what you do when the provider is down.

Submitting a home assignment that only handles the happy path, with no pagination, timeouts, retries or documentation

Treat the submission as production code. Handle empty and malformed responses and rate limiting, add tests with a stubbed HTTP layer, and document assumptions and trade-offs in the README, because candidates report that these details come up in follow-up conversations.

Naming a database or pattern without saying why, for example choosing NoSQL for metadata because it scales

For each choice, state the access patterns, the consistency you need and the alternative you rejected. A defensible split is relational metadata with indexes on the lookup keys and file content in object storage, referenced by key and checksum.

Questions & practice

Collapse a redelivered event batch into per-aggregate high-water marks

Easy · hashing · at-least-once · aggregation

You drain a batch of up to 5,000,000 events, each (aggregate_id BIGINT, aggregate_version INT, event_type, payload). The log guarantees order within one aggregate only; the batch merges 64 partitions, and a relay failover has redelivered a range, so an older version for an aggregate can appear after a newer one. Given a map of last_applied_version per aggregate, produce the events worth applying, at most one per (aggregate_id, version), plus the count discarded. Target O(n) time. State the memory for 2,000,000 distinct aggregates and what you do when it does not fit.

Approach

  1. One pass, one hash map from aggregate_id to the highest version kept, and a discard counter. An event whose version is at or below last_applied_version for its aggregate is dropped without further work, which is the whole reason the event carries its version rather than a delta. O(n) expected time, O(d) space in distinct aggregates.
  2. Keep the maximum, never the last occurrence. The redelivered range means the final appearance of an aggregate in the batch can be an older version than one seen earlier in the same batch, so last-wins applies stale state over newer state and the projection regresses with no error anywhere.
  3. Cost the memory instead of calling it large: an 8-byte key plus a 4-byte version is 12 bytes of payload, and an open-addressed table held at a 0.7 load factor costs roughly 17 bytes per entry before per-slot metadata, so 2,000,000 aggregates is tens of megabytes in a native layout and several times that in a runtime that boxes both key and value.
  4. If the distinct set exceeds memory, partition on hash(aggregate_id) mod P and reduce each partition independently. Every event for one aggregate hashes to the same partition, so the per-partition result is exact and the merge is concatenation rather than a second reduction.

Follow-up

  • The payload is a patch rather than a snapshot, so applying only the highest version loses the intermediate changes. What changes in your reduction?
  • How do you detect that version 7 arrived while version 6 was never delivered, and what should the consumer do about the gap?

Archive a resource graph without breaking live references or recursing

Medium · graph traversal · topological order · tenant isolation

Resources reference other resources within a tenant; for the largest tenant the reference table holds up to 2,000,000 nodes and 8,000,000 edges. Archiving a resource must archive everything reachable from it that nothing outside the set still references, refuse when a live external referrer exists, and terminate when references form cycles, which they legitimately do. Produce the archive order and the refusal list, targeting O(V+E). Say what stops the traversal crossing a tenant boundary, and why recursion is the wrong control structure at this size.

Approach

  1. Load the subgraph with the tenant predicate on both endpoints of the edge, not only on the side you started from. Scoping the left table alone is the classic cross-tenant leak: one mis-entered edge then pulls another tenant's resources into the traversal and, worse, into the archive.
  2. Traverse iteratively with an explicit stack. A 2,000,000-node graph can hold a chain deep enough to exhaust a native stack in the low tens of thousands of frames, and that failure is a process crash rather than an error you can return.
  3. Treat cycles as data rather than corruption: compute strongly connected components with Tarjan in O(V+E) using its own explicit stack, then condense. The condensation is a DAG, so a topological order over it gives the archive order, and every member of a component archives in one transaction because no order within a cycle is valid.
  4. Decide refusals with reverse edges. A candidate is archivable only if every in-edge originates inside the candidate set, so build the transpose or count in-degrees restricted to the visited set, and emit each blocked resource with the id of the external referrer, which is the only part of the answer an operator can act on.

Follow-up

  • The graph is read in one query and the archive writes a minute later. What can change in between, and how do you make the write safe?
  • The candidate set is 400,000 resources. Is that one transaction, and if not, what does a half-finished archive look like to a reader?

Track a rolling failure rate per destination for circuit decisions

Easy · sliding window · ring buffer · circuit breaker

The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.

Approach

  1. Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
  2. Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
  3. State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
  4. Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.

Follow-up

  • The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
  • A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?

Stop tag and share joins from fanning out a page

Medium · join fan-out · semi-joins · many-to-many

resource_tag is (resource_id, tag_id) with PK (resource_id, tag_id); resource_share is (resource_id, shared_with_user_id, permission). The tagged-and-shared listing inner-joins resource to both, filters tenant_id, tag_id = ANY($2) and shared_with_user_id = $3, orders by updated_at DESC and takes 50. Pages come back with fewer than 50 distinct resources and the total in the header is far too high. Explain the row multiplication, rewrite both the page query and the count query so each is correct, and name the index each one needs. PostgreSQL 16.

Approach

  1. Do the arithmetic against the predicates that are actually there. An inner join emits one row per matching child row, and both joins are filtered: tag_id = ANY($2) admits only the requested tags, shared_with_user_id = $3 admits one user's share rows. So a resource holding three of the requested tags and shared with $3 once yields three rows, not one — the multiplier is its count of matching tags times its share rows for that single user, and that second factor is 1 unless the table admits duplicate (resource_id, shared_with_user_id) pairs. LIMIT 50 then limits rows rather than resources, and COUNT(*) counts pairs — the header is the product, not the population.
  2. Reject DISTINCT as the fix. It deduplicates after the product has been built, so the planner must materialise and sort the fanned-out set before the LIMIT can apply, and it leaves any SUM or AVG in the same select list wrong.
  3. Rewrite both filters as semi-joins, keeping resource as the only row source: AND EXISTS (SELECT 1 FROM resource_tag rt WHERE rt.resource_id = r.resource_id AND rt.tag_id = ANY($2)) and the same shape against resource_share. A semi-join stops at the first match per resource and preserves the driving index order, so ORDER BY updated_at DESC, resource_id DESC LIMIT 50 still stops after 50 rows.
  4. Count with the same predicates and no join at all: SELECT count(*) FROM resource r WHERE r.tenant_id = $1 AND r.status = 'active' AND EXISTS (...) AND EXISTS (...). Nothing multiplies a resource, so the number is the population.

Follow-up

  • The filter changes from 'any of these tags' to 'all of these tags'. Rewrite it and state what it costs relative to the ANY form.
  • A resource can be shared with the same user twice under different permissions. Does your count change, and should it?

Hold a per-tenant active cap against concurrent creates

Hard · write skew · isolation levels · constraints

A tenant on the standard plan may hold at most 50 resources with status='active'. The create handler runs SELECT count(*) FROM resource WHERE tenant_id = $1 AND status = 'active', compares to 50, then inserts. Two creates arrive 3 ms apart on different instances and the tenant lands at 51. Name the anomaly, say whether PostgreSQL 16 READ COMMITTED or REPEATABLE READ prevents it and why, then give an implementation that holds the cap at READ COMMITTED with the exact statements. Finally, say what changes when the cap is 'at most one running export per tenant' on job_run.

Approach

  1. Name it: write skew. The two transactions read an overlapping set and write disjoint rows, so there is no row-level conflict for the engine to detect and each commit is individually legal.
  2. Rule out the levels precisely. READ COMMITTED takes a fresh snapshot per statement and takes no lock on the counted rows, so both see 49. PostgreSQL's REPEATABLE READ is snapshot isolation: it removes non-repeatable reads and phantoms within the snapshot but still admits write skew, because the anomaly is not a re-read of a changed row, it is a read of a set that a concurrent transaction invalidates. Only SERIALIZABLE closes it, by tracking the read dependency and aborting one transaction with SQLSTATE 40001 — a guarantee that exists only if the application re-runs the whole transaction from the read.
  3. Convert the set predicate into a single-row conflict: keep tenant.active_resource_count and run UPDATE tenant SET active_resource_count = active_resource_count + 1 WHERE tenant_id = $1 AND active_resource_count < 50 in the same transaction as the INSERT. Zero affected rows is the cap, returned as 409. The row lock serialises the decision at any isolation level, and contention is bounded to one tenant's row — which is also the fair-scheduling unit, unlike a global counter that would convoy every tenant behind one row.
  4. State the cost you just took on: a counter is a second source of truth that can drift, so every path that changes status must adjust it inside the same transaction, and a periodic reconciliation has to exist, with resource_revision as the authority for what the count should have been.

Follow-up

  • A resource moves from archived back to active. Which statements change, and what breaks if the counter update and the status change land in different transactions?
  • The cap becomes plan-dependent and a plan can change mid-month. Where does the number 50 live, and who reads it?

Design a document management application with version control capabilities, including API endpoints and databa

Medium · Technical & Architectural Design

Design a document management application with version control capabilities, including API endpoints and database schema.

Approach

  1. Pin down scope first: who edits (single owner or shared), file types (text or binary), and whether old versions can be restored, diffed or purged. Those answers decide the schema and the storage choice.
  2. Use two tables. documents(id, tenant_id, owner_id, title, current_version_id, created_at, deleted_at) and document_versions(id, document_id, version_number, blob_key, content_hash, size_bytes, author_id, created_at, change_note), with UNIQUE(document_id, version_number). Versions are immutable rows. The bytes live in object storage under blob_key, not in the row.
  3. Endpoints: POST /documents creates the document and its version 1. GET /documents/{id} returns metadata plus the latest version. POST /documents/{id}/versions adds a version. GET /documents/{id}/versions lists versions with cursor pagination. GET /documents/{id}/versions/{n} fetches one version. POST /documents/{id}/versions/{n}/restore copies version n forward as a new latest version rather than rewriting history.
  4. Handle concurrent edits with optimistic locking. The client sends the version it edited (If-Match with an ETag, or base_version in the body). In one transaction the server inserts version base+1 and moves current_version_id. The unique constraint makes the losing writer fail, and it gets 412 (If-Match) or 409 so it can rebase. Add an Idempotency-Key header so a retried upload does not create two versions.
  5. Since the product area is compliance, state an assumption aloud: stored versions may later serve as audit evidence. Then make versions immutable (append-only rows, or object-lock/WORM storage for the blobs), let retention follow whatever rules the applicable framework sets, and record every purge or soft delete in an audit log of who did it and when. Add per-document permissions and presigned uploads committed only after confirmation.

Follow-up

  • Two users save edits based on the same version at the same moment. What exactly happens to each request, and what does the losing client see?
  • How would you show a diff between two versions, and would you compute it on write or on read?
  • How does the schema change if documents can be shared across tenants or organised into folders?

If a system currently sends emails to 10 users but needs to scale to 100,000, what architectural changes would

Medium · Technical & Architectural Design

If a system currently sends emails to 10 users but needs to scale to 100,000, what architectural changes would you implement?

Approach

  1. Take sending out of the request path. The API call records a send job and returns 202. A fan-out worker pages through recipients with keyset pagination and puts batched messages on a durable queue (SQS, RabbitMQ or Kafka). A pool of stateless workers takes messages off the queue and calls the email provider.
  2. Plan for at-least-once delivery. Keep a sends table keyed by UNIQUE(campaign_id, recipient_id) with status pending/sent/failed. Each worker claims a row before calling the provider and passes an idempotency key where the provider supports one. This narrows duplicates but does not remove them: a crash after the provider accepts the message and before the row is marked sent can still send one duplicate on redelivery, unless the provider deduplicates on the key. Say which residual risk you accept.
  3. Throttle to the provider's quota with a token bucket shared across workers, and size the job with arithmetic. At a hypothetical limit of 50 sends per second, 100,000 emails take about 2,000 seconds, roughly 33 minutes. Adding workers does not beat the quota.
  4. Split failures by type. 429 and 5xx responses are retried with exponential backoff and jitter. Hard bounces and invalid addresses are not retried and go to a suppression list. Messages that keep failing go to a dead-letter queue. Check unsubscribes and suppressions before each send, and use bounce and complaint webhooks to update the list.
  5. Instrument queue depth, send rate, failure rate by error class, and age of the oldest pending send. Put transactional mail (password resets) on a separate queue from bulk mail so a large batch cannot hold it up.

Follow-up

  • A worker crashes after the provider accepted the message but before the row was marked sent. What happens on redelivery, and how much duplication can you tolerate?
  • How would you send each recipient the email at 9am in their own time zone?
  • The provider is down for an hour in the middle of the batch. What does the system do, and what does the requester see?

How would you handle the storage and retrieval of specific file versions (e.g., version 50 vs. the latest vers

Medium · Technical & Architectural Design

How would you handle the storage and retrieval of specific file versions (e.g., version 50 vs. the latest version)?

Approach

  1. Name the three storage options and their costs. Full snapshots make any version a single fetch, but storage grows with versions times file size. A forward delta chain is compact, but rebuilding version n means applying every delta from the base, which is O(n). Reverse deltas (as RCS does) keep the latest version whole and store backward diffs, so the latest is cheap and old versions cost more.
  2. Recommend a hybrid with checkpoints. Store a full snapshot every k versions and deltas in between, so rebuilding any version applies at most k-1 deltas. For version 50 with k = 10, load the snapshot at 50 directly; for version 57, load 50 and apply 7 deltas. Binary formats that diff poorly can be stored as full snapshots and deduplicated by content hash.
  3. Make lookup a single index probe. UNIQUE(document_id, version_number) finds version 50 with one B-tree lookup. Get the latest from a current_version_id pointer on the document row, or from ORDER BY version_number DESC LIMIT 1 on the same index. Never scan the history to find the latest.
  4. Use content-addressed blob keys (SHA-256 of the bytes) so identical versions share one object. If you use S3 object versioning, keep your own version_number to S3 version-id mapping, because S3 version IDs are opaque strings, not sequence numbers.
  5. Tier by access pattern. Cache the latest version, move old versions to a cheaper storage class with lifecycle rules, and accept slower retrieval for them. Cover edge cases: a version purged by retention (410 Gone), a version number that does not exist (404), and two writers assigning the same next number.

Follow-up

  • Autosave creates a new version every few seconds. How do you keep version counts and storage under control?
  • How would you return a diff between version 49 and version 50, and where is it computed?
  • A user restores version 50. Does that rewrite history or create a new version, and why?

What is the best strategy for separating metadata storage from raw data in a high-volume system?

Medium · Technical & Architectural Design

Approach

  1. Put metadata in a relational database: id, tenant_id, owner, content type, size, content hash, storage key, status and timestamps, indexed for the queries the product runs, such as (tenant_id, created_at). Put raw bytes in object storage under immutable keys. Large blobs in table rows bloat the table, backups and replication, and compete for the database's cache.
  2. Order the write path to survive partial failure. Insert a metadata row with status pending, upload the bytes (directly from the client through a presigned URL), then mark the row committed once the upload is confirmed by a HEAD check or a storage event. Readers only see committed rows.
  3. Clean up the gaps, because the two stores share no transaction. A periodic sweeper deletes blobs that have no committed row past a grace period and expires pending rows whose upload never arrived. Deletes become a tombstone on the row first, then an asynchronous purge of the object.
  4. On reads, query metadata and return a short-lived presigned GET URL so the bytes never pass through the app servers. Build derived data (extracted text, thumbnails, search index) in asynchronous consumers of upload events, not in the upload request.
  5. State the database trade-off. Relational fits metadata that needs joins, constraints and ad hoc filters. A key-value store fits key-only access at very high write rates but loses those queries. At volume, partition metadata by tenant or time, and avoid hot object-key prefixes if the storage backend is sensitive to them.

Follow-up

  • How do you guarantee there are no orphaned blobs and no metadata rows pointing at missing objects?
  • Users now need full-text search inside the raw files. Where does that index live and how is it kept current?
  • How would you isolate tenants' raw data, for example with per-tenant encryption keys or buckets?

p99 jumped on one listing filter while p50 stayed flat

Medium · query planning · composite index · percentiles

After a release that added an owner_user_id filter to the resource listing, p99 rose from 90 ms to 1.9 s while p50 stayed at 40 ms. Traffic and row counts are unchanged. resource carries the index (tenant_id, status, updated_at DESC, resource_id DESC). The new query filters tenant_id and owner_user_id, orders by updated_at DESC, resource_id DESC, and takes 20 rows. On PostgreSQL, explain the shape of the regression, prove it from a query plan, and give the index you would add.

Approach

  1. Start from the shape. A flat p50 with a moved p99 means a subset of requests changed cost, not all of them, so the first job is naming the subset. Bucket the endpoint's latency by the tenant's row count; the natural hypothesis is that large tenants are a small share of requests and all of the tail.
  2. Get the plan for the new query on a large tenant with EXPLAIN (ANALYZE, BUFFERS). Expect an index scan over the tenant's range, a filter discarding most of it, then a Sort feeding the Limit, possibly reporting Sort Method: external merge Disk. Read actual rows on the scan node, not estimated.
  3. Explain why the existing index cannot serve it. A composite B-tree is seekable only as a left prefix, and with no equality predicate on status the scan cannot treat updated_at as an ordering, because rows in the tenant's range are ordered by status first. Everything matching must be read and sorted before LIMIT 20 can apply, so a tenant with 400,000 rows pays 400,000 rows to return 20.
  4. Add (tenant_id, owner_user_id, updated_at DESC, resource_id DESC). Equality on the first two columns leaves the index ordered by updated_at within that pair, so the plan becomes an index scan that stops after 20 rows with no Sort node. PostgreSQL can scan a B-tree backwards, so the DESC markers matter only if the two sort columns ever disagree in direction; keeping them explicit documents the order the keyset cursor depends on.

Follow-up

  • The endpoint paginates with OFFSET. What does page 500 cost with your index, and what does the keyset version cost?
  • How would you have caught this before release, given that a 10,000-row seed database produces the same plan shape at an unnoticeable cost?

Suggested practice checklist

The last two days cover explaining your decisions and a full spoken dry run.

Day 1 — Home assignment foundations

  • Write a small client for any public paginated JSON API in Python, Node.js or Go: a pagination loop, request timeout, retry with exponential backoff and jitter on 429 and 5xx responses only, and validation of each response.
  • Add tests with a stubbed HTTP layer for an empty page, malformed JSON, a rate-limit response and records repeated across pages, then write a README with run steps, assumptions and what you would change for production.
  • Solve the practice drill on collapsing a redelivered event batch into per-aggregate high-water marks, which uses the same ingestion habits.

Deliverable: A repository holding the API client, its tests and README, plus a passing solution to the event batch drill.

Day 2 — Version history data model

  • Write DDL for a documents table (id, owner, current_version) and an append-only versions table keyed by (document_id, version_number) with content reference, size, checksum, author and created_at.
  • Write the queries for version 50, the latest version, and a paginated version list, and note the index each one uses.
  • Compare a full snapshot per version with deltas plus periodic snapshots: write what reading version 50 costs under each and when you would choose each.

Deliverable: Schema DDL, three queries with their indexes, and a one-paragraph snapshot versus delta trade-off, which together answer the question about retrieving a specific file version.

Day 3 — REST API and concurrent edits

  • Write the endpoint table for the document service: create a document, add a version, get the latest, get version n, list versions with a cursor. Give request fields and status codes.
  • Add optimistic concurrency: the client sends the version it edited from, the server inserts the next version under the unique constraint, and a stale writer receives a conflict response carrying the current version.
  • Answer the full document management question aloud, covering API, schema, versioning and the conflicting-edit scenario, and write down every trade-off you stated.

Deliverable: An endpoint table with status codes, a written conflict-handling flow, and one spoken answer to the version control design question.

Day 4 — Scaling the email sender

  • Draw the 10-user design (send inside the request) and the 100,000-user design: enqueue, worker pool, email provider, per-recipient status table, with an idempotency key so a retried job does not send twice.
  • Write the failure handling: batching, provider rate limits, retries with backoff, a dead-letter queue, and what happens during a provider outage or a sudden spike in load.
  • Solve the practice drill on tracking a rolling failure rate per destination, which maps onto deciding when to stop sending to a failing provider.

Deliverable: A before and after diagram, a written answer to the email scale-up question that names the failure modes, and a passing solution to the rolling failure rate drill.

Day 5 — Metadata versus raw data, indexing, and cloud mapping

  • Design the split: metadata in a relational database, file content in object storage addressed by key and checksum. Write the upload flow (store the blob, then commit the metadata row) and a cleanup job for orphaned blobs.
  • List the indexes and partitioning choices for the metadata tables, such as (document_id, version_number) and (tenant_id, updated_at), and say which query each one serves.
  • Map your reference design onto AWS or GCP managed services: SQS or Pub/Sub for the queue, S3 or GCS for file content, RDS or Cloud SQL for metadata. Write how the workers are containerised with Docker and scaled on Kubernetes, for example by queue depth.
  • Work the practice drills on stopping joins from fanning out a page and on a p99 spike on one listing filter, then answer the metadata separation question aloud.

Deliverable: A written answer to the metadata versus raw data question with its failure modes and index list, a one-page cloud and Kubernetes mapping of the reference design, plus solved SQL and debugging drills.

Day 6 — Explaining the why, and your own stories

  • For each of the four reported design questions, write two sentences in the form 'I chose X over Y because ...', tying the choice to a product or business reason.
  • Write two or three projects as stories: the problem, the constraints, the decision you made, the result. Choose ones you can discuss in depth for a long conversation with a team lead.
  • Write a short, honest account of how you use AI coding tools, including what you hand off and how you verify the output, then rehearse the practice behavioral drills on deadline trade-offs and on disagreeing with a design.

Deliverable: Four decision statements, two or three written project stories, a short account of your AI tool use, and spoken answers to the two behavioral drills on shipping under a deadline and on arguing against a design.

Day 7 — Full spoken dry run

  • Answer the four reported design questions aloud back to back, in the order of version control application, retrieving a version, metadata separation, email scaling, drawing as you go.
  • Answer two scenarios candidates report: how data integrity holds when several users edit the same document version, and how you handle a sudden spike in system load.
  • Write the questions you will ask your recruiter, such as the exact stages, the language for the home assignment and who is in the interview day, and the questions you will ask leadership about the product roadmap.

Deliverable: A recorded or written-out dry run of all four design answers and the two scenarios, plus a list of questions to ask.

Stories and decisions to explain

Candidates report that the interview day includes behavioral alignment and technical discussions with team leads and leadership, and that you should be able to explain why you chose a technical path for the business. No specific behavioral questions are reported, so prepare stories where you made the decision and can state the alternative and the outcome.

  1. Walk through a complex challenge you solved in a past project, from the constraints to the result.
  2. Tell me about a technical decision where you chose a simpler option over a more scalable one, and what the business reason was.
  3. Describe how you use AI tools in your coding, and a time you rejected or corrected what a tool produced.
  4. Describe a time you gave or received critical code review feedback and what changed afterwards.
  5. Tell me about a technical bottleneck you spotted without being asked and how you resolved it.
  6. Explain a design you built to a non-engineer or a senior leader and how you checked they understood the trade-offs.

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?

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?

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

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