Senior Engineering Interview Handbook / Chapter 59
Take-Home Assignments
A worked cart-promotion assignment shows how to budget a take-home, choose a rule-bearing slice, test it, document it, review it from a clean checkout, and defend its trade-offs.
Preparing audio…
Audio edition
Take-Home Assignments
Page tools
When the clock stops
Four hours have passed. The repository is now sitting on somebody else’s laptop. You are not there to explain why a directory exists, which command to run, or whether an unfinished endpoint was meant to count. The reviewer sees only the prompt, the files, and the behavior they can reach.
That absence is the defining constraint of a take-home assignment. More time than a live round does not turn it into a small product launch. It gives you a chance to leave a compact trail of evidence: a complete slice, important rules under test, exact operating instructions, a comprehensible history, and boundaries you can defend later.
If the prompt sets a time limit, honor it. Reserve part of that limit for the submission rather than spending the final minute pushing whatever happens to compile. For a four-hour exercise, a plausible budget is thirty minutes to read and scope, two and a half hours to build and test, and one hour to review, document, and package. The exact split can change. The protected principle is that delivery work belongs inside the timebox.
Before opening an editor, extract the assignment’s contract:
- required behavior and explicit exclusions;
- language, framework, runtime, and submission format;
- effort limit, deadline, and timezone;
- services, libraries, or tools that are required or forbidden;
- any disclosure rule the company gives you.
Ambiguity is not permission to guess silently. Choose a reasonable assumption, record it where the reviewer will see it, and make sure the solution remains coherent under that assumption.
Find the rule-bearing slice
Consider this prompt:
Build a small service or application that lets a user create a cart, add items, apply a promotion code, and see the final total. Explain your trade-offs.
The nouns suggest an entire commerce system: accounts, catalog management, inventory, persistence, checkout, tax, payment, receipts, and deployment. None of that expansion makes the required behavior more complete. It creates more surfaces to leave shallow.
The useful center is the promotion rule. A bounded submission can create an in-memory cart, add and remove items, apply one active promotion, calculate money in integer cents, and reject unknown, expired, or duplicate codes. It can offer a small API or command-line interface, depending on the prompt. That is a finished path with enough resistance to reveal design judgment.
Write the boundary down early:
Implemented
- Create an in-memory cart and add or remove line items.
- Apply one active percentage promotion.
- Reject unknown, expired, and duplicate promotions.
- Return subtotal, discount, and total in integer cents.
Not implemented
- Accounts, persistence, tax, inventory, payment, and deployment.
The second list is not an apology. It prevents the reviewer from having to distinguish deliberate scope from forgotten work. It also gives you a test for every tempting addition: does this machinery protect the chosen rule or make the assignment easier to inspect? If not, it waits.
Docker, a database, authentication, a queue, CI, or a cloud deployment may be required by a particular brief. Otherwise each one spends time and adds a new failure mode. A one-command local program is stronger evidence than a production-shaped diagram around unfinished behavior.
Let the invariant choose the tests
Start with the behavior whose failure would make the submission untrustworthy. For the cart, applying the same promotion twice must not reduce the total twice. Expired and unknown codes must leave the cart unchanged. Removing an item must update both the subtotal and the derived discount.
Those rules suggest the design. Promotion validation should happen before cart mutation. Money should have an explicit rounding policy and a representation that does not introduce binary floating-point surprises. The tests should observe returned totals and state transitions, not merely that a mock method was called.
A small, persuasive test set would show that:
- a valid promotion changes subtotal, discount, and total as specified;
- a duplicate application is rejected without changing the cart;
- expired and unknown promotions are rejected without mutation;
- removing an item recalculates the promotion correctly;
- boundary amounts follow the stated rounding rule.
The prompt may call for a different kind of artifact. A user-interface task needs its central interaction plus empty, loading, error, and relevant accessibility states. A data task needs malformed and boundary rows along with deterministic output. A workflow needs valid and invalid transitions, retry or cancellation, and terminal behavior. The question remains the same: which tests let a reviewer trust the rule that carries the assignment?
Coverage percentage cannot answer that question. When time is short, one test around a consequential invariant is worth more than many assertions about constructors, accessors, or mocks.
Make the repository explain itself
The README is not promotional copy. It is the route through the evidence. A reviewer should learn, in this order, what exists, how to run it, how to test it, where the main rule lives, and what has deliberately been left out.
For the cart service, the useful part might read:
Implemented scope
- In-memory carts with add and remove operations.
- One active percentage promotion per cart.
- Rejection of unknown, expired, and duplicate promotions.
Run
1. npm install
2. npm test
3. npm run dev
Design decisions
- Amounts are stored in integer cents. Percentage discounts use the rounding
rule documented in src/money.
- Promotion validation is separate from total calculation so rejection occurs
before cart mutation.
- Storage is in memory because persistence is outside the requested slice.
Known limitations
- Promotion data is seeded at startup.
- The single-process model does not protect concurrent updates.
- Tax, inventory, authentication, and payment are outside this assignment.
If the work continued
- Put cart updates behind a persistence and concurrency boundary.
- Define promotion stacking rather than assuming one active code.
- Add request-level tests around validation and error responses.
Use exact commands and name runtime prerequisites. Document environment variables and provide safe sample values where the program needs them. Do not make the reviewer discover seed data, a local service, or a hidden setup step from an error message.
Keep design notes close to the decisions they explain. A short README section is enough for the cart’s representation and validation boundary. A separate decision note earns its place only when alternatives and consequences need more room. Comments should explain local constraints in code, not carry the submission’s missing architecture story.
Leave a legible path through the work
When repository history is part of the submission, commits become another review surface. They need not pretend the implementation emerged without a false start. They should let a reader see coherent increments.
For this exercise, the history could move through project setup and a test
harness, the first cart path, promotion rules and failure cases, and finally
documentation and cleanup. A single enormous final commit conceals those
boundaries. Dozens of format-fix and typo commits make them harder to find.
Commit when the work reaches an intelligible state. Before submitting, remove unrequested build output, caches, secrets, and accidental dependency files. Squash noise if the instructions and submission method allow it, but do not rewrite history merely to manufacture a flawless performance. A reviewer needs a clear trail, not theatre.
Review the handoff, not your memory
The most valuable final test begins outside the working directory. Clone or copy the submission into a clean location, then follow only the README. Install dependencies, run the tests, start the program, and exercise the main path. This is where undocumented environment variables, missing fixtures, global tools, stale generated output, and machine-specific assumptions become visible.
Then read the original prompt once more. For every required behavior, point to the implementation and its test or verification path. If a requirement is neither present nor explicitly excluded for a defensible reason, it is not done. Fix that gap before adding polish.
Now review the repository as if you had fifteen minutes between meetings:
- Can you identify the implemented slice from the first screen of the README?
- Do the documented commands work without private knowledge?
- Can you locate the rule that gives the exercise its difficulty?
- Do the tests prove that rule and its damaging failure cases?
- Does every abstraction correspond to a present requirement or credible variation?
- Are limitations paired with concrete consequences rather than a vague wish to make the project “production ready”?
This pass often produces better senior evidence by removing things. An unused repository interface, speculative event bus, or empty deployment directory does not demonstrate foresight. It asks the reviewer to inspect possibility instead of finished behavior.
Defend boundaries without inflating them
A useful trade-off note has three parts: what you omitted, which risk that choice leaves, and where the capability would enter if the system continued.
“Persistence was out of scope” is incomplete. For the cart, a better account is:
I kept carts in memory to finish and test the promotion rules inside the assignment window. The result does not survive process restart and cannot coordinate concurrent writers. I would preserve the cart interface, put its mutations behind a transactional repository, and add a version check or other concurrency policy before treating the service as shared production state.
That answer is specific without pretending you built the extension. Apply the same discipline to authentication, observability, deployment, performance, or security. Name the actual exposure and the boundary that would own it.
In a follow-up conversation, present the submission in the order a reviewer can verify it: chosen scope, working path, governing rule, tests, one or two design decisions, and remaining risks. Be ready to change a constraint. What if promotions can stack? What if two requests update one cart? What if the brief now requires persistence? A senior defense does not insist that the take-home is already a production system. It shows that the current design was proportionate and that you know where it would have to change.
Rehearse the whole artifact
Choose a small prompt such as a meeting-room booking service, URL shortener, expense splitter, or task queue. Give yourself two hours:
- fifteen minutes to extract constraints and write the README skeleton;
- one hour for a complete path and tests around its central rule;
- twenty-five minutes for failure cases and cleanup;
- fifteen minutes for design notes and known limitations;
- five minutes for a clean-checkout run.
Afterward, ask another engineer to review the repository without speaking to you. Watch where they hesitate, which command fails, and what they cannot infer from the tests. Repair the artifact rather than explaining it aloud.
A take-home ends before the review conversation begins. The durable evidence is therefore modest and exact: a slice small enough to finish, rules important enough to test, instructions another person can follow, history they can scan, and limits stated with enough precision to become the next engineering decision.
Continue reading
Full table of contents