Welcome. This is a small backend take-home you should be able to finish in 2–3 hours.
You are joining a partially-built HTTP service that moves money between wallets stored in MySQL. The server boots, the database connects, the read-only endpoints work, and there is a full test suite. Most transfer-related tests fail on a fresh clone — making them pass is the task.
We care about correctness and judgment, not lines of code. It is much better to ship a small, coherent, correct solution with clear reasoning than a large, half-working one.
- Implement
POST /transfersandGET /transfers/:idin src/services/transfers.js. - Make the failing tests under test/ pass, especially the concurrency and idempotency tests.
- Write a short
NOTES.mdexplaining the tradeoffs you made.
Everything else — server, routes, DB pool, schema, seed, test harness — is already provided.
Prerequisites: Node.js 20+, Docker Desktop (or a local MySQL 8).
# 1. Start MySQL in the background
docker compose up -d
# 2. Copy environment defaults
cp .env.example .env # (Windows: copy .env.example .env)
# 3. Install dependencies
npm install
# 4. Create the schema and load seed accounts
npm run db:reset
# 5. Run the server
npm start
# → wallet service listening on http://localhost:3000
# 6. In another terminal, run the tests
npm testOn a fresh clone, npm test should:
- Pass all tests in
test/accounts.test.js. - Fail most tests in
test/transfers-basic.test.js,test/transfers-idempotency.test.js, andtest/transfers-concurrency.test.jswith a clearTODO: implement transferMoneyerror.
That failing state is your starting point.
| Piece | Location | Status |
|---|---|---|
| Express server + factory | src/server.js, src/app.js | done |
| MySQL pool + transaction helper | src/db.js | done |
GET /health |
src/app.js | done |
GET /accounts/:id |
src/routes/accounts.js | done |
GET /accounts/:id/transactions |
src/routes/accounts.js | done |
POST /transfers route wiring |
src/routes/transfers.js | done — calls the stub below |
GET /transfers/:id route wiring |
src/routes/transfers.js | done — calls the stub below |
| Schema (accounts, transfers) | db/schema.sql | done — you may modify, justify in NOTES.md |
| Seed data (Alice $100, Bob $50, Carol $0) | db/seed.sql | done |
| Full test suite (Mocha + Chai + Supertest) | test/ | done — do not modify existing tests |
| Docker Compose (MySQL 8.4) | docker-compose.yml | done |
| Piece | Location | Status |
|---|---|---|
transferMoney({ from, to, amount, idempotencyKey }) |
src/services/transfers.js | stub — throws TODO |
getTransfer(id) |
src/services/transfers.js | stub — throws TODO |
NOTES.md at the repo root |
you create this | required |
The route handlers already call these functions and translate their return values into HTTP responses. You should not need to change the routes; if you do, keep the contract intact.
Request
POST /transfers
Content-Type: application/json
Idempotency-Key: <optional string, up to 80 chars>
{
"from_account_id": 1,
"to_account_id": 2,
"amount_cents": 500
}Success response — 201 Created
{
"id": 42,
"idempotency_key": "abc-123",
"from_account_id": 1,
"to_account_id": 2,
"amount_cents": 500,
"status": "completed",
"failure_reason": null,
"created_at": "2026-07-01T12:00:00.000Z"
}This is the row from the transfers table.
Error responses
| Status | When |
|---|---|
404 Not Found |
from_account_id or to_account_id does not exist |
422 Unprocessable Entity |
Missing field, non-positive amount_cents, self-transfer, insufficient funds |
200 OK or 201 Created |
Idempotency-Key seen before — return the original transfer. Pick one status and be consistent. |
500 Internal Server Error |
Unhandled bug. A correct solution must not return 500 under concurrent load. |
Error body shape: { "error": "<short code>" }. Suggested codes: account_not_found, insufficient_funds, self_transfer, invalid_amount.
200 OKwith the transfer row, or404 Not Foundif it does not exist.
Returns { id, owner_name, balance_cents, created_at }, or 404.
Returns up to 100 recent transfers involving the account (as source or destination), newest first.
Returns { "ok": true } when the DB responds.
These are the properties the tests verify. Your implementation must satisfy all of them.
- Atomicity — a transfer either updates both balances and writes the
transfersrow, or has no effect at all. - Non-negative balances — no account may ever go below zero, including under concurrent load.
- Money conservation — for any sequence of transfers between the seeded accounts, the sum of balances is unchanged.
- Concurrency (the interesting one) — the concurrency suite fires 100 concurrent transfers of $1 from an account that only has $50. Exactly 50 must succeed. The sender must end at $0 and the receiver at +$50.
- Deadlock handling — the suite fires transfers between the same two accounts in both directions concurrently. Your handler must not return
500. Either avoid deadlocks (deterministic lock ordering) or catchER_LOCK_DEADLOCKand retry. - Idempotency — retries with the same
Idempotency-Keymust not debit the sender twice. The tests fire 10 parallel retries of the same key; exactly one transfer row must be created and all responses must return that same row.
Read the test files — they are the executable specification.
- Do not modify the existing tests. You are welcome and encouraged to add more tests (e.g. edge cases you thought of that we did not).
- You may change the schema — add columns, indexes, or new tables — but keep the migration in db/schema.sql and justify the change in
NOTES.md. - Adding libraries is fine, but keep the dependency list small and defend anything unusual.
- Design decisions are yours. The stub in src/services/transfers.js documents the contract but not the how. Pick a locking strategy, pick an idempotency approach, and explain why in
NOTES.md. - If you run out of time, stop and write
NOTES.md. A working 70% with clear reasoning beats an incoherent 100%.
Submit:
- Your code (all changes on top of this scaffold).
- A
NOTES.mdat the repo root answering:- Locking / concurrency strategy — what did you use (
SELECT ... FOR UPDATE, conditionalUPDATE, application-level lock, …) and why? How does it handle the two-account deadlock scenario? - Idempotency approach — how do you detect a replay? What happens if the same key arrives with a different body?
- Schema tradeoffs — you are storing balance directly on the
accountsrow. What are the pros/cons vs. an append-only ledger (entries table + derived balance)? Would you change it for production? - One thing you would do differently with more time.
- One bug or limitation you know is still in your code.
- Locking / concurrency strategy — what did you use (
We read NOTES.md closely. It is where the "communicating tradeoffs" signal comes from.
In rough order of weight:
- Correctness under concurrency — do the concurrency tests pass, would they still pass under 10× load?
- DB and schema judgment — indexes, constraints, transaction boundaries, choice of locking.
- API contract discipline — sensible status codes, consistent error shape, input validation at the boundary.
- Code quality — organization, readability, no obvious footguns.
NOTES.md— the depth and honesty of your tradeoff reasoning matters as much as the code.
We do not score on: number of features shipped beyond the requirements, code style religion, or comment density.
| Command | What it does |
|---|---|
docker compose up -d |
Start MySQL in the background. |
docker compose down -v |
Stop MySQL and wipe the data volume. |
npm run db:reset |
Drop tables, re-apply schema, reload seed. |
npm start |
Run the API on port 3000. |
npm test |
Run the Mocha suite. Resets DB between every test. |
src/
server.js entry point
app.js express app factory (used by tests too)
db.js mysql2/promise pool + withTransaction helper
errors.js HttpError class used by the error middleware
routes/
accounts.js complete
transfers.js wired, delegates to services/transfers.js
services/
transfers.js <-- STUB. This is what you implement.
db/
schema.sql accounts + transfers tables
seed.sql Alice ($100), Bob ($50), Carol ($0)
test/
setup.js resets DB before each test
accounts.test.js passes on a fresh clone
transfers-basic.test.js happy path + validation
transfers-idempotency.test.js Idempotency-Key behavior
transfers-concurrency.test.js concurrent load, no double-spend, no deadlocks
scripts/
reset-db.js used by `npm run db:reset`
Good luck — and thanks for taking the time.