Skip to content

accept-blue/coding-test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wallet Transfer — Backend Coding Challenge

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.


TL;DR — what you are building

  1. Implement POST /transfers and GET /transfers/:id in src/services/transfers.js.
  2. Make the failing tests under test/ pass, especially the concurrency and idempotency tests.
  3. Write a short NOTES.md explaining the tradeoffs you made.

Everything else — server, routes, DB pool, schema, seed, test harness — is already provided.


Setup

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 test

On 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, and test/transfers-concurrency.test.js with a clear TODO: implement transferMoney error.

That failing state is your starting point.


What is already provided

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

What you build

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.


API contract

POST /transfers

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.

GET /transfers/:id

  • 200 OK with the transfer row, or
  • 404 Not Found if it does not exist.

GET /accounts/:id — already implemented

Returns { id, owner_name, balance_cents, created_at }, or 404.

GET /accounts/:id/transactions — already implemented

Returns up to 100 recent transfers involving the account (as source or destination), newest first.

GET /health — already implemented

Returns { "ok": true } when the DB responds.


Correctness requirements

These are the properties the tests verify. Your implementation must satisfy all of them.

  1. Atomicity — a transfer either updates both balances and writes the transfers row, or has no effect at all.
  2. Non-negative balances — no account may ever go below zero, including under concurrent load.
  3. Money conservation — for any sequence of transfers between the seeded accounts, the sum of balances is unchanged.
  4. 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.
  5. 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 catch ER_LOCK_DEADLOCK and retry.
  6. Idempotency — retries with the same Idempotency-Key must 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.


Ground rules

  • 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%.

Deliverable

Submit:

  1. Your code (all changes on top of this scaffold).
  2. A NOTES.md at the repo root answering:
    • Locking / concurrency strategy — what did you use (SELECT ... FOR UPDATE, conditional UPDATE, 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 accounts row. 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.

We read NOTES.md closely. It is where the "communicating tradeoffs" signal comes from.


How we evaluate

In rough order of weight:

  1. Correctness under concurrency — do the concurrency tests pass, would they still pass under 10× load?
  2. DB and schema judgment — indexes, constraints, transaction boundaries, choice of locking.
  3. API contract discipline — sensible status codes, consistent error shape, input validation at the boundary.
  4. Code quality — organization, readability, no obvious footguns.
  5. 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 reference

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.

Project layout

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.

About

Code Test for potential hirees

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages