Please refer to coding-exercise.md for the full problem description and instructions.
Create your solution in a fork of this repository. Once you're ready to submit, please add dmanning-resilient as a collaborator on your private repository and send us a message.
npm install
npm test # vitest — unit + integration tests
npm run typecheck # tsc --noEmitCSV payload
→ CallHandler (thin handler — parse, validate, ack <500ms)
→ BatchDispatcher (fire-and-forget — kicks off async processing)
→ BatchProcessor (pipeline: enrich → persist → project)
├─ CallRecordEnricher (parallel operator lookups, partial enrichment)
├─ CallRecordRepository (source of truth, idempotent upsert)
└─ SearchIndex (eventually consistent projection)
- Zod as single source of truth —
CallRecordandEnrichedCallRecordtypes are inferred from schemas, no manual interfaces. - Sub-500ms ack — handler does synchronous validation only; enrichment + storage runs in background via dispatcher.
- Partial enrichment —
Promise.allSettledon operator lookups; one failure doesn't block the other or the whole batch. - Retry with bounded attempts —
OperatorLookupwraps the flaky API (2 retries, 100ms delay). - Repository = commit point — search index failure never rolls back persisted data, only marks
projectionRetryPending. - Ports & adapters — infrastructure behind interfaces (
CallRecordRepository,SearchIndex), wired via constructor injection. - In-memory implementations — stubs for repository (Map-based, idempotent) and search index (array-based); in production these would be e.g. PostgreSQL and Elasticsearch.
- No real queue/message broker — dispatcher uses fire-and-forget
Promise; production would use e.g. BullMQ or SQS. - No projection retry mechanism —
projectionRetryPendingis tracked but not acted on; a real system would have a scheduled reconciler. - Batch-atomic validation — a single invalid record rejects the entire batch; this is intentional to prevent silent partial ingestion.
I used AI to generate the code. I used the following tools: