Welcome, and thanks for taking the time. This is a focused take-home built to give us signal on how you model a small system, handle messy real-world concerns (CSV input, flaky external APIs), and communicate trade-offs.
~4 hours. Hard cap. We are not testing whether you can build a production-grade system in a weekend — we're testing what you choose to do with limited time.
If you hit the 4-hour mark and something is unfinished, stop and write it down in the README (What I would do with more time). That section is graded; an honest list of cuts beats a half-broken extra feature.
A small backend service that lets a marketing operator:
- Create an SMS campaign (template + sender info).
- Upload a CSV of recipients and kick off sending.
- Check campaign status (queued / sent / failed counts).
A campaign blasts a templated SMS to a list of customers. Sending hits a (mocked) SMS provider that occasionally fails, so the service has to handle retries and surface partial failures cleanly.
Hubby sells eSIMs. Each customer SIM has an ICCID (a 19–20 digit SIM identifier) which we use as the unique key for a recipient. For this challenge assume an ICCID maps 1:1 to a phone number internally — you do not need to look up or validate phone numbers. Treat the ICCID as the recipient ID you pass to the SMS provider.
- Node.js + TypeScript
- Express (or Fastify if you prefer — your call)
- SQLite for storage (via
better-sqlite3,prisma,drizzle, or raw SQL — your call). No cloud DB, no Docker required. - Vitest or Jest for tests
- No Firebase / Supabase / Twilio / queues / Redis. Keep it in-process.
If you have a strong reason to deviate, fine — explain it in the README.
Just three. Design the request/response shapes yourself.
Create a campaign with a message template.
- Template supports variables in
{{name}}form, e.g.Hi {{first_name}}, your data plan ends on {{expiry}}. - Variables are filled in per-recipient from the CSV columns (see below).
Upload a CSV of recipients and start sending.
- CSV has a header row. Required column:
iccid. Any other columns become template variables (e.g.first_name,expiry). - Reject the upload (with a useful error) if the template references a variable the CSV doesn't supply.
- Sending should happen in the background — the request returns quickly with the count of accepted recipients. The actual SMS dispatch can run in-process; you do not need a real queue.
Return the campaign config plus aggregate progress: total recipients, queued, sent, failed, and (optional) last error message.
Do not integrate with a real SMS API. Build against this interface:
export interface SmsClient {
send(input: { iccid: string; body: string }): Promise<{ providerMessageId: string }>;
}Provide a FakeSmsClient implementation that:
- Resolves after a small random delay (e.g. 50–200ms).
- Fails ~10% of the time by throwing an error (mix of "transient" — should retry — and "permanent" — should not). You decide how to model that distinction; we want to see the design.
Keeping the interface narrow and the fake pluggable is part of what we're evaluating.
- CSV parsing — don't load gigantic files into memory if you can help it (streaming preferred), but a 10k-row file is the realistic ceiling here. Don't over-engineer.
- Rate limiting — a simple bounded concurrency (e.g. send N at a time with a small delay) is fine. Document the choice.
- Retries — transient failures should retry with backoff up to a small max. Permanent failures should not.
- Validation — bad CSV, missing template variables, unknown campaign id, etc. Return useful errors.
- Persistence — campaigns, recipients, and per-recipient send status should survive a restart. (You don't need to resume in-flight sends after a crash; just note it as a known limitation if you skip it.)
We are not asking for full coverage. Pick one or two areas you found genuinely tricky — for most candidates that's CSV parsing, template rendering, or retry logic — and write good tests there. We care more about test design than count.
- A public GitHub repo (preferred) or zip.
- A
README.mdcontaining:- How to install, run, and test (
npm install && npm test && npm startideally). - Design notes — the 3–5 trade-offs that mattered most, in 1–2 sentences each.
- What I would do with more time — explicit list of cuts and known limitations.
- Roughly how many hours you actually spent. Be honest; we don't penalize either way.
- How to install, run, and test (
- A sample CSV in
examples/so we can run it end-to-end.
| Area | Weight |
|---|---|
| Domain modeling & TypeScript usage | 20 |
| Correctness of the core send flow | 20 |
| Error handling & input validation | 15 |
| Test quality (focused, not exhaustive) | 15 |
| README clarity & setup ergonomics | 10 |
| Stated trade-offs & self-awareness | 10 |
| Code structure & readability | 10 |
We do not score on: deployment, auth, fancy frameworks, line count, or features beyond what's listed.
Pick at most one. Ship the core well first.
- Idempotent recipient upload — re-uploading the same CSV doesn't re-send to recipients already sent.
- Webhook endpoint for delivery receipts updating recipient status.
- Pagination on a
GET /campaigns/:id/recipientsendpoint.
- Small, readable modules. Clear types at boundaries. No
any-soup. - Errors say what went wrong and what the caller should do.
- The fake SMS client is swappable behind an interface — no
if (process.env.NODE_ENV)branching in business logic. - A README we can run in two commands.
- A short, honest list of things you cut.
If something is ambiguous, make a call, document the assumption in the README, and move on. We'd rather see decisive trade-offs than a Slack thread of clarification questions — but if you're truly blocked, email us.
Good luck. Have fun with it.