XcelCrowd Technical Challenge Submission
Next In Line is a self-healing hiring pipeline designed for zero-manual intervention. Each job opening has a hard active-review capacity. When capacity is exhausted, the system does not reject additional applicants; it enqueues them on a deterministic waitlist. When an active slot is released, the queue advances automatically. When a promoted applicant fails to acknowledge within the decay window, the system penalizes and requeues them, promotes the next candidate, and continues the cascade without operator involvement.
This submission is intentionally engineered around four non-negotiable properties:
- Atomic capacity enforcement
- Deterministic queue ordering
- Bounded decay behavior
- Reconstructable state transitions
The implementation uses PERN because PostgreSQL is not just storage here; it is the concurrency control layer, the queue ordering primitive, and the audit backbone.
- Executive Summary
- Evaluator Fast Path
- The Hard Problems
- Architectural Blueprint
- Zero-Friction Setup
- Workflow and Commands
- Repository Layout
- API Documentation
- Testing and Verification
- Demo Walkthrough
- Submission Checklist
- Engineering Retrospective
- Lead Engineer Assessment
Treat the hiring pipeline as a transactional queueing system, not a spreadsheet.
That design choice drives the entire implementation:
- queue position is not inferred in the UI; it is materialized in the database
- concurrency is not handled by request timing luck; it is serialized with row locks
- timeouts are not ephemeral timers; they are processed by a bounded decay worker
- auditability is not an afterthought; every transition appends an immutable log record
The codebase is built to maintain these invariants:
- A job can never exceed its configured active capacity.
- Waitlist order is deterministic and monotonic.
- An applicant can never occupy more than one non-exited application for the same job.
- Every domain transition is appended to
StateLogs. - Decay cannot recurse forever; it is bounded by
MAX_DECAYS.
PostgreSQL gives this challenge the exact primitives it needs:
- Transactions for atomic operations
FOR UPDATErow locks for race-condition control- Sequences for deterministic queue ordering
- Partial unique indexes for duplicate protection
SKIP LOCKEDfor safe decay scanning
Express keeps the queue engine readable at the HTTP boundary, React is sufficient for a polling-based operations dashboard, and Node.js is a practical fit for a stateful I/O-heavy API plus a lightweight heartbeat worker.
The frontend is intentionally dynamic but not real-time. It uses periodic polling rather than sockets because this challenge optimizes for operational clarity and correctness over push-delivery complexity. For a small internal hiring tool, a deliberate polling cadence is the right trade-off.
If reviewing time is limited, these are the highest-signal files:
- Queue engine: backend/services/queueService.js
- Decay worker: backend/services/decayManager.js
- Schema and indexes: backend/models/schema.sql
- Application endpoint: backend/controllers/jobController.js
- Applicant ownership and status APIs: backend/controllers/applicantController.js
- Integration tests: backend/tests/concurrency.test.js
If you want to validate the submission quickly:
- Start the stack with
docker-compose up --build - Create a job with capacity
1 - Apply twice with different applicant emails
- Confirm one applicant lands in
PENDING_ACKand the other inWAITLISTED - Trigger an exit from the active applicant
- Confirm the next applicant promotes automatically
- Wait for the dashboard refresh window or manually refresh the applicant view
- Inspect
StateLogsor the dashboard audit terminal to replay the sequence
The waitlist is implemented without BullMQ, Redis, cron schedulers, or any third-party queue abstraction.
Instead, the queue is modeled directly in PostgreSQL using a sequence-backed ordering key:
priority_scoreis stored per applicantpriority_scoredefaults tonextval('priority_score_seq')- waitlist order is
ORDER BY priority_score ASC - promotion selects the smallest
priority_scoreamongWAITLISTEDapplicants - decay reassigns a fresh sequence value, which becomes the penalty operation
Schema excerpt:
CREATE SEQUENCE IF NOT EXISTS priority_score_seq;
CREATE TABLE Applicants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID REFERENCES Jobs(id),
email VARCHAR(255) NOT NULL,
status applicant_status NOT NULL,
priority_score BIGINT NOT NULL DEFAULT nextval('priority_score_seq'),
decay_count INT NOT NULL DEFAULT 0,
last_transition_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_job_status_priority
ON Applicants(job_id, status, priority_score ASC) INCLUDE (id);Source: backend/models/schema.sql
The challenge-critical race condition is the last-spot collision:
- Job capacity =
1 - Two applications arrive nearly simultaneously
- Both see an apparently free slot
- Without synchronization, both could transition into the active set
This implementation solves the race condition inside the database transaction boundary with pessimistic locking.
The critical logic is:
- Begin transaction
- Lock the target
Jobsrow withFOR UPDATE - Count current slot-consuming applicants:
ACTIVE+PENDING_ACK - Decide state atomically:
- below capacity ->
PENDING_ACK - at capacity ->
WAITLISTED
- below capacity ->
- Insert applicant row
- Commit
Code excerpt:
await client.query('BEGIN');
const jobRes = await client.query(
'SELECT capacity FROM Jobs WHERE id = $1 FOR UPDATE',
[jobId]
);
const countRes = await client.query(
"SELECT COUNT(*) FROM Applicants WHERE job_id = $1 AND status IN ('ACTIVE', 'PENDING_ACK')",
[jobId]
);
let status = 'WAITLISTED';
if (currentCount < capacity) {
status = 'PENDING_ACK';
}
await client.query(
'INSERT INTO Applicants (job_id, email, status) VALUES ($1, $2, $3) RETURNING id',
[jobId, email, status]
);
await client.query('COMMIT');Source: backend/services/queueService.js
Because the job row is locked before occupancy is read, writers for the same job are serialized. The second request cannot evaluate stale capacity while the first request still holds the lock.
This is a database-backed atomic decision, not an application-layer best effort.
- Duplicate non-exited applications are blocked by:
CREATE UNIQUE INDEX idx_unique_active_application
ON Applicants(job_id, email) WHERE status != 'EXITED';- Automatic promotions also lock the same job row before moving a waitlisted applicant into
PENDING_ACK, which prevents cascade-driven overfill.
When a waitlisted applicant is promoted into active review, they have to acknowledge. If they do nothing, the system must:
- keep capacity moving
- avoid losing the applicant entirely
- penalize their queue position
- avoid infinite recycling
The decay window is environment-driven:
ACK_WINDOW_HOURS, default24
Expiry condition:
last_transition_at < NOW() - ($2::int * INTERVAL '1 hour')Source: backend/services/decayManager.js
The backend starts an in-process heartbeat worker when the server boots:
- interval:
60_000 ms - batch size:
100 - selection strategy:
FOR UPDATE SKIP LOCKED
This means expired applicants are processed in batches, and if multiple workers ever exist, locked rows are skipped rather than double-processed.
Timeout behavior for a PENDING_ACK applicant:
await client.query(
"UPDATE Applicants SET status = 'WAITLISTED', priority_score = nextval('priority_score_seq'), decay_count = $2, last_transition_at = NOW(), updated_at = NOW() WHERE id = $1",
[id, decay_count + 1]
);That has two effects:
statusbecomesWAITLISTEDpriority_scoregets a new sequence value
Because waitlist ordering is ascending by priority_score, the applicant is pushed to the back of the queue in a deterministic way.
After each timeout or active-slot release, the system calls:
await queueService.promoteNext(job_id, client);Importantly, this is executed inside the same transaction as the decay or exit operation. That means slot release and queue advancement are committed atomically.
Decay is bounded by:
MAX_DECAYS, default3
Once decay_count >= MAX_DECAYS, the applicant transitions to EXITED instead of being recycled forever:
if (decay_count >= MAX_DECAYS) {
await queueService.transitionApplicant(
client,
id,
'PENDING_ACK',
'EXITED',
'MAX_DECAY_PURGE'
);
}This makes the decay algorithm predictable and terminal.
Applicant movement is modeled explicitly as domain state transitions.
Valid transition paths currently implemented:
null -> PENDING_ACKon apply when capacity existsnull -> WAITLISTEDon apply when capacity is fullWAITLISTED -> PENDING_ACKonSYSTEM_PROMOTIONPENDING_ACK -> ACTIVEonUSER_ACKPENDING_ACK -> WAITLISTEDonTIMEOUT_DECAYPENDING_ACK -> EXITEDonMAX_DECAY_PURGEACTIVE -> EXITEDonUSER_EXITWAITLISTED -> EXITEDonUSER_EXITPENDING_ACK -> EXITEDonUSER_EXIT
The queue service centralizes transition logging via:
await this.logStateChange(client, applicantId, fromStatus, toStatus, trigger);That is what makes the domain timeline reconstructable.
Concrete example for a job with capacity 1:
- Applicant A applies first.
- occupancy =
0 - Applicant A ->
PENDING_ACK - log:
null -> PENDING_ACK (USER_APPLIED)
- occupancy =
- Applicant B applies second.
- occupancy =
1 - Applicant B ->
WAITLISTED - log:
null -> WAITLISTED (USER_APPLIED)
- occupancy =
- Applicant A never acknowledges within
24hours.- Applicant A ->
WAITLISTED - Applicant A gets a new
priority_score - log:
PENDING_ACK -> WAITLISTED (TIMEOUT_DECAY)
- Applicant A ->
- The same transaction immediately promotes the next waitlisted applicant.
- Applicant B ->
PENDING_ACK - log:
WAITLISTED -> PENDING_ACK (SYSTEM_PROMOTION)
- Applicant B ->
- Applicant B acknowledges.
- Applicant B ->
ACTIVE - log:
PENDING_ACK -> ACTIVE (USER_ACK)
- Applicant B ->
That is the core self-healing property of the system.
flowchart LR
A["React Applicant Portal"] --> B["Express API"]
C["React Company Dashboard"] --> B
B --> D["Queue Service"]
B --> E["Applicant Controller"]
D --> F["PostgreSQL"]
G["Decay Manager"] --> F
D --> H["StateLogs"]
G --> H
stateDiagram-v2
[*] --> PENDING_ACK : Apply when capacity is available
[*] --> WAITLISTED : Apply when capacity is full
WAITLISTED --> PENDING_ACK : SYSTEM_PROMOTION
PENDING_ACK --> ACTIVE : USER_ACK
PENDING_ACK --> WAITLISTED : TIMEOUT_DECAY
PENDING_ACK --> EXITED : MAX_DECAY_PURGE
WAITLISTED --> EXITED : USER_EXIT
PENDING_ACK --> EXITED : USER_EXIT
ACTIVE --> EXITED : USER_EXIT
Primary domain tables:
JobsApplicantsStateLogs
Operationally important fields:
Jobs.capacityApplicants.statusApplicants.priority_scoreApplicants.decay_countApplicants.last_transition_at
Important indexes:
idx_job_status_priorityfor promotion and queue position queriesidx_decay_checkfor heartbeat expiry scansidx_unique_active_applicationfor duplicate non-exited application prevention
| Operation | Time Complexity | Bottleneck |
|---|---|---|
| Promotion Lookup | O(log N) | Index Scan |
| Application Decision | O(1) | Row Lock |
| Decay Scan | O(K) | SKIP LOCKED scan |
This is the right complexity profile for a small-team internal tool with strict correctness requirements.
The UI is deliberately thin. It does not own queue logic and it does not attempt optimistic reordering.
- The company dashboard polls pipeline state on a fixed interval.
- The applicant portal polls status and renders queue position returned by the backend.
- All authoritative ordering and state transition decisions remain server-side.
That matters because it keeps race-condition handling in one place: the transactional backend. The frontend is a consumer of state, not a co-author of state.
- Node.js
18+ - npm
9+ - PostgreSQL
15+or Docker Desktop psqlCLI if running manually
Run the full stack:
docker-compose up --buildServices:
- Frontend:
http://localhost:3001 - Backend:
http://localhost:5000 - PostgreSQL:
localhost:5432
cd backend
npm installCreate backend/.env:
DATABASE_URL=postgresql://postgres:password@localhost:5432/nextinline
PORT=5000
JWT_SECRET=super_secure_key_123_competition
ACK_WINDOW_HOURS=24
MAX_DECAYS=3Bootstrap the database:
Tip
If prompted for a password by psql, use the one defined in your .env (default in seeds/compose is password).
# Using psql (Ensure the 'nextinline' database exists)
psql -U postgres -c "CREATE DATABASE nextinline;"
# Apply schema and initial seeds
psql -U postgres -d nextinline -f models/schema.sql
psql -U postgres -d nextinline -f models/seed.sqlStart the backend:
npm startcd frontend
npm installCreate frontend/.env:
PORT=3001
REACT_APP_API_URL=http://localhost:5000/apiStart the frontend:
npm startBackend:
cd backend
npm startFrontend:
cd frontend
npm startcd backend
npm testcd frontend
npm run buildpsql -U postgres -d nextinline -f backend/models/schema.sql
psql -U postgres -d nextinline -f backend/models/seed.sqlThe repository is intentionally small and evaluator-friendly:
next-in-line/
├── backend/
│ ├── controllers/ # HTTP boundary and auth-aware request handling
│ ├── services/ # Queue engine and decay worker
│ ├── models/ # PostgreSQL schema and seed data
│ ├── schemas/ # Zod validation rules
│ ├── middleware/ # auth, validation, and error handling
│ ├── config/ # Database and Logger configuration
│ ├── utils/ # Reusable helper functions
│ └── __tests__/ # integration coverage for high-risk flows
├── frontend/
│ ├── src/pages/ # company dashboard and applicant portal
│ ├── src/hooks/ # React Query data access layer
│ ├── src/components/ # Reusable UI primitives (Command Center cards)
│ └── src/api.js # API client configuration
├── docker-compose.yml # local full-stack boot path
├── .env.example # template for local environment secrets
└── README.md # architecture and evaluator guide
If I were onboarding another engineer, I would point them to these files first:
- backend/services/queueService.js
- backend/services/decayManager.js
- backend/models/schema.sql
- backend/tests/concurrency.test.js
- frontend/src/pages/CompanyCommandCenter.jsx
- frontend/src/pages/ApplicantPortal.jsx
The system exposes a RESTful API for both applicants and company administrators. For full request/response schemas, please refer to the dedicated API.md file.
POST /api/jobs/:id/apply
Authorization: Bearer <jwt>
Content-Type: application/json
{}Possible response:
{
"id": "application-uuid",
"status": "WAITLISTED"
}GET /api/applicants/:id/status
Authorization: Bearer <jwt>Possible response:
{
"id": "application-uuid",
"job_id": "job-uuid",
"email": "jane@example.com",
"status": "WAITLISTED",
"priority_score": 42,
"decay_count": 1,
"last_transition_at": "2026-04-24T00:00:00.000Z",
"position": 3
}The backend test suite currently validates the highest-risk behaviors:
- Concurrent last-slot applications
- Applicant ownership isolation
- Automatic promotion after active-slot release
Test file:
Verified scenarios:
50parallel applicants compete for a job with capacity1- Exactly one applicant lands in
PENDING_ACK - The remaining applicants land in
WAITLISTED - Queue ordering remains unique and collision-free
- One applicant cannot read another applicant's status
- Exiting an active applicant triggers immediate promotion of the next waitlisted candidate
This is important because the README is not merely describing intended behavior; the highest-risk parts of the design are covered by integration tests.
If I were presenting this submission to a hiring panel or recording a Loom, I would use this flow:
- Open the company dashboard and show a fresh job with capacity
1. - Open two applicant sessions with different emails.
- Submit applicant A, then submit applicant B.
- Show that applicant A is
PENDING_ACKand applicant B isWAITLISTED. - Call out that this is not timing luck; it is enforced by a transaction plus
FOR UPDATE. - Withdraw applicant A or let applicant A timeout.
- Show applicant B automatically move into
PENDING_ACK. - Open the audit view and replay the transitions from
StateLogs.
- The queue advances without manual intervention.
- Capacity is never exceeded.
- A timed-out applicant is not deleted; they are penalized and requeued.
- Every movement is traceable with a trigger label.
- The frontend reflects backend state rather than fabricating it locally.
“Two applicants are competing for a single active slot. The first request acquires the job lock, evaluates occupancy, and inserts into PENDING_ACK. The second request blocks on the same row lock, re-evaluates after commit, and lands in WAITLISTED. If the active applicant exits or times out, the release and promotion happen in the same transaction, so the queue heals itself without an operator touching it.”
Before sharing this repository with an evaluator, I would verify the following:
docker-compose up --buildboots frontend, backend, and PostgreSQL without manual fixesbackend npm testpasses locallyfrontend npm run buildcompletes successfully- the README links and Mermaid diagrams render on GitHub
- the demo flow shows one applicant in
PENDING_ACKand one inWAITLISTED - the audit trail visibly records
USER_APPLIED,SYSTEM_PROMOTION,USER_ACK,TIMEOUT_DECAY, andUSER_EXIT - the Loom recording stays under five minutes and follows the scripted flow above
If adding visuals before submission, these are the only screenshots I would include:
-
Company dashboard showing one active applicant and one waitlisted applicant for the same job

-
Applicant portal showing queue position and acknowledgement countdown state

-
Audit or transition history view showing reconstructable state changes

Those three images tell the whole story without clutter.
- The decay worker is an in-process heartbeat poller instead of a distributed scheduling system.
- The authentication layer is intentionally lightweight and local-first; it is not a production identity platform.
- SQL bootstrap files are used instead of a formal migration framework.
- The frontend uses deliberate polling rather than WebSockets or SSE.
- The queue engine is optimized for correctness on a single PostgreSQL primary rather than globally distributed writes.
- A migration framework such as Prisma Migrate, Knex migrations, or Flyway
- WebSockets or SSE for lower-latency admin visibility
- A distributed worker or leader-election strategy for multi-instance decay processing
- Job-level configuration for acknowledgement windows and decay thresholds
- More domain events and analytics around conversion, timeout rate, and queue churn
- A richer reviewer workflow beyond queue management
- A dedicated applicant timeline endpoint that replays
StateLogsdirectly
From a senior reviewer perspective, this submission solves the right problem in the right layer.
The strongest architectural choice is that the queue is not simulated in JavaScript memory. It is modeled as transactional state in PostgreSQL. The concurrency path uses pessimistic locking where the challenge actually needs it. The decay algorithm is bounded, deterministic, and auditable. The transition log is append-only and sufficient for replay.
Most importantly, the code demonstrates an understanding that distributed-systems bugs in small products usually begin as state-management shortcuts. This implementation avoids that trap by grounding the hardest behaviors in database-backed atomic operations.