Skip to content

hubbyesim/bechallenge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 

Repository files navigation

Hubby Backend Challenge — Bulk SMS Campaign Service

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.

Time box

~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.

What we're building

A small backend service that lets a marketing operator:

  1. Create an SMS campaign (template + sender info).
  2. Upload a CSV of recipients and kick off sending.
  3. 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.

Domain note (so the field names make sense)

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.

Tech stack (fixed, so we can compare submissions fairly)

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

Endpoints

Just three. Design the request/response shapes yourself.

POST /campaigns

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).

POST /campaigns/:id/recipients

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.

GET /campaigns/:id

Return the campaign config plus aggregate progress: total recipients, queued, sent, failed, and (optional) last error message.

The SMS provider (mocked)

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.

What we expect you to handle

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

Tests

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.

Deliverable

  • A public GitHub repo (preferred) or zip.
  • A README.md containing:
    • How to install, run, and test (npm install && npm test && npm start ideally).
    • 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.
  • A sample CSV in examples/ so we can run it end-to-end.

How we score (out of 100)

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.

Stretch goals (only if you have time left — clearly optional)

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/recipients endpoint.

What "good" looks like to us

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

Questions

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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors