Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

27 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

LoadUp Challenge - Job Applicant Selection System

โš™๏ธ Environment Configuration

Copy .env.example to .env and adjust if needed:

cp .env.example .env

๐Ÿš€ **Quick Start (1-2 commands):**

```bash
# 1. Setup everything (database, dependencies, seed data)
npm run setup # Runs install, db:up, migrate, and seed

# 2. Start the API server
npm run dev
# Open http://localhost:3000/docs for API documentation

โœ… LoadUp Challenge Checklist

  • Fastify + TypeScript project with Dockerized PostgreSQL
  • Prisma schema, migrations, and seed data (1000+ applicants)
  • CRUD routes for companies, branches, jobs, applicants
  • Assessment system with templates & submissions (answer snapshots)
  • Matching algorithm with negative marking, recency boost, location bonus
  • Deterministic tie-breaking & explainable results
  • Top 5 candidates endpoint (/jobs/:id/top-candidates)
  • API documentation at /docs (Swagger UI with parameter schemas)
  • Request-level tests (happy path, validation errors, 404s)
  • Structured error handling and correlation ID logging
  • Production-ready README with setup, architecture, and scoring explanation
  • 1-2 command setup (npm run setup + npm run dev)

๐Ÿ—๏ธ Architecture & Tech Stack

Runtime: Node.js 20+ with TypeScript (ES modules)
Framework: Fastify 5.6 (lightweight, fast) with Swagger/OpenAPI integration
Database: PostgreSQL via Prisma ORM with Docker Compose
Validation: Zod schemas for type-safe request/response validation
Logging: Pino with request correlation IDs and structured error handling
Testing: Vitest + Supertest for end-to-end API testing

Why these choices:

  • Fastify: Superior performance vs Express, built-in validation, excellent TypeScript support
  • Prisma: Type-safe ORM, great migrations, prevents SQL injection by design
  • Zod: Runtime validation with TypeScript inference, perfect for APIs
  • Docker Compose: Consistent local PostgreSQL environment

๐Ÿ—„๏ธ Domain Model

This shows how the main business entities relate to each other in the LoadUp recruitment system:

Company (1) โ†โ†’ (N) Branch
   โ†“
Job (N) โ†โ†’ (1) AssessmentTemplate โ†โ†’ (N) AssessmentItem  
   โ†“                                      โ†“
Applicant (N) โ†โ†’ (N) Submission โ†โ†’ (N) SubmissionAnswer

Real-World Example:

  • DHL Logistics (Company) has Hamburg Branch + Berlin Branch (Branches)
  • "Delivery Driver" job (Job) uses "Driving Skills Test" (AssessmentTemplate)
  • Anna Schmidt (Applicant) takes the test, creating a Submission with her answers
  • The matching algorithm finds the best candidates for "Delivery Driver at Hamburg Branch"

Key Design Decisions:

  • Jobs belong to companies but are matched against specific branches (flexible hiring)
  • Assessment templates are reusable across multiple jobs (efficient test management)
  • Submissions snapshot correctness and weights at submission time (audit trail)
  • Scoring uses pure SQL aggregation for performance and determinism

๐Ÿ”Œ API Endpoints

Core CRUD Operations

  • Companies: POST|GET|DELETE /companies (with search & pagination)
  • Branches: POST|GET|DELETE /branches (filter by companyId)
  • Jobs: POST|GET|DELETE /jobs (linked to assessment templates)
  • Applicants: POST|GET|DELETE /applicants (searchable by name/email/city)

Assessment System

  • Templates: POST /assessment-templates (create with nested items)
  • Submissions: POST /submissions (validate against template, record answers)

Matching & Analytics

  • Match Candidates: POST /matches (sophisticated scoring algorithm)
  • Top Candidates: GET /jobs/:id/top-candidates (cached-friendly)
  • Platform Stats: GET /stats (usage metrics)

Top Candidates Design: /jobs/:id/top-candidates is a thin wrapper around /matches with sensible defaults (limit=5). For larger scale, this endpoint is an ideal candidate for Redis caching with short TTL, as "top 5" queries are frequently repeated by hiring managers.

Full API Documentation: Available at http://localhost:3000/docs (Swagger UI)

Documentation Integration: OpenAPI schemas are generated from Zod validation definitions and auto-mounted via Fastify Swagger. This ensures validation rules and documentation stay perfectly synchronized - any schema change automatically updates both runtime validation and API documentation.

๐Ÿงฎ Scoring Algorithm

Formula: total = raw + recencyBoost + locationBonus

Components:

  • Raw Score: correctWeight - (negativeMarkingFraction ร— wrongWeight)
  • Recency Boost: raw ร— recencyBoostPct / 100 (if within window)
  • Location Bonus: locationBonusFixed + (raw ร— locationBonusPctOfRaw / 100)

Configurability (runtime, no code changes):

{
  "negativeMarkingFraction": 0.25,    // 25% penalty for wrong answers
  "recencyWindowDays": 30,            // Recent = last 30 days
  "recencyBoostPct": 10,              // 10% boost for recent submissions
  "locationBonusFixed": 5,            // +5 points for local candidates  
  "locationBonusPctOfRaw": 0          // Percentage bonus (disabled by default)
}

Deterministic Tie-Breaking: total DESC โ†’ raw DESC โ†’ lastSubmissionAt DESC โ†’ applicantId ASC

Full Explainability: Each result includes detailed breakdown of score components.

Example API Usage

Note: Replace jobId and branchId with actual values from your seeded data. Get real IDs via: curl "http://localhost:3000/jobs?limit=1" and curl "http://localhost:3000/branches?limit=1"

Request:

curl -X POST http://localhost:3000/matches \
  -H "Content-Type: application/json" \
  -d '{
    "jobId": "cltxyz123abc456",
    "branchId": "cltabc789def012",
    "limit": 5,
    "filters": { "cityEqualsBranch": false },
    "scoring": {
      "negativeMarkingFraction": 0.25,
      "recencyWindowDays": 30,
      "recencyBoostPct": 10,
      "locationBonusFixed": 5,
      "locationBonusPctOfRaw": 0
    }
  }'

Response (example with realistic seed data):

{
  "matches": [
    {
      "applicantId": "cltdef456ghi789",
      "applicantName": "Anna Schmidt",
      "applicantEmail": "anna.schmidt@example.com", 
      "applicantLocation": "Berlin",
      "totalScore": 87.4,
      "explanation": "Raw: 72.0 (9 correct, 2 wrong, -2.8 penalty) + Recency: 14.4 (20% of raw, submitted 5 days ago) + Location: 1.0 (5 fixed + 0% of raw, Berlin != Hamburg)",
      "rawScore": 72.0,
      "recencyBoost": 14.4,
      "locationBonus": 1.0
    }
  ],
  "meta": {
    "jobId": "cltxyz123abc456",
    "jobTitle": "Delivery Driver",
    "branchId": "cltabc789def012",
    "branchName": "DHL Logistics - Hamburg Branch", 
    "branchLocation": "Hamburg",
    "totalCandidates": 847,
    "scoringConfig": {
      "negativeMarkingFraction": 0.25,
      "recencyWindowDays": 30,
      "recencyBoostPct": 10,
      "locationBonusFixed": 5,
      "locationBonusPctOfRaw": 0
    }
  }
}

๐Ÿ“Š Data Seeding

# Default: ~1000 applicants, 5 companies, realistic submissions
npm run seed

# Large dataset: 5000+ applicants for performance testing  
SEED_APPLICANTS=5000 npm run seed

Configurable via Environment Variables (set in .env file):

SEED_COMPANIES=4      # Number of logistics companies (default: 4)
SEED_BRANCHES=2       # Branches per company (default: 2) 
SEED_JOBS=2          # Jobs per company (default: 2)
SEED_APPLICANTS=1000 # Total applicants (default: 1000)
SEED_TEMPLATES=3     # Assessment templates (default: 3)
SEED_ITEMS=10        # Items per template (default: 10)

Realistic Data Generated:

  • German logistics companies (DB Schenker, DHL, Hermes, GLS)
  • Geographic clustering (Berlin, Munich, Hamburg, Cologne)
  • 85% submission coverage with weighted assessment items
  • Time-distributed submissions for recency testing

๐Ÿงช Testing

# Run all tests
npm test

# Test categories
npm test health.stats.test.ts     # Health checks & stats
npm test errors.validation.test.ts # Error handling & validation  
npm test matches.happy.test.ts    # Matching algorithm end-to-end

LoadUp Challenge E2E/Request-Level Test Requirements:

  • โœ… Health & Stats endpoints: Complete system health verification and platform metrics
  • โœ… Matching happy path: Full candidate ranking workflow with realistic data and explainable results
  • โœ… Validation & error cases: Input validation, HTTP status codes, and structured error responses

Comprehensive Coverage:

  • โœ… Happy path: Full matching workflow with realistic data
  • โœ… Edge cases: Validation errors, 404s, empty results, company mismatches
  • โœ… Determinism: Same inputs produce same candidate ordering
  • โœ… Algorithm: Scoring components, configurability, and tie-breaking logic

๐Ÿ›ก๏ธ Production Readiness

โš ๏ธ Challenge Project Disclaimer: While the system is production-ready in architecture and design patterns, it is not hardened for production deployment (no authentication, CI/CD pipeline, or operational monitoring). This showcases technical capabilities within challenge scope.

Error Handling:

  • Consistent HTTP status codes (400, 404, 409, 500)
  • Structured error responses with field-level validation details
  • No stack trace leakage to clients

Logging & Observability:

  • Request correlation IDs (x-request-id header support)
  • Structured JSON logging with Pino
  • Error tracking with request context

Performance:

  • Pagination caps (max 100 items per request)
  • Database indexes on search fields and foreign keys
  • SQL-based scoring (no N+1 queries)
  • Handles 100,000+ applicants efficiently

Security:

  • Input validation on all endpoints (Zod schemas)
  • SQL injection prevention (Prisma parameterized queries)
  • Basic operational hygiene

๐Ÿ”„ Development Workflow

# Start development server (auto-reload)
npm run dev

# Database management
npm run db:up     # Start PostgreSQL in Docker
npm run migrate   # Apply schema migrations  
npm run seed      # Generate test data

# Production build
npm run build
npm start

๐Ÿš€ Deployment Ready

The system is production-ready with:

  • Docker Compose setup for local development
  • Environment variable configuration
  • TypeScript compilation for production builds
  • Structured logging for monitoring
  • Database migrations for schema management

๐Ÿ“ Assumptions & Trade-offs

Simplifications:

  • Jobs belong to companies; branches specified at match time (vs separate JobOpening entity)
  • One assessment template per job (vs template reuse across jobs)
  • No authentication/authorization (focus on core matching logic)

Performance Trade-offs:

  • OFFSET pagination (vs keyset) - simpler but slower for deep pages
  • Eager scoring calculation (vs precomputed rankings) - flexible but compute-intensive

Scaling Considerations:

  • Read replicas for high query volume
  • Caching for /top-candidates queries
  • Keyset pagination for large result sets
  • Background job processing for bulk operations

๐ŸŽฏ Future Enhancements

  • Authentication: JWT-based auth with role-based access control
  • Caching: Redis for hot queries and session management
  • Real-time: WebSocket updates for live candidate rankings
  • Analytics: Advanced reporting and candidate insights
  • API Versioning: Backward compatibility for client integrations

๐Ÿค– AI Assistance

I used AI assistance (GitHub Copilot) during this challenge to speed up development.
Specifically, it helped me with:

  • Structuring the project setup (Fastify, Prisma, Docker)
  • Drafting boilerplate for routes, validation, and seed scripts
  • Iterating on the SQL scoring logic
  • Debugging test failures and Postgres syntax issues
  • Polishing documentation (README)

All final design decisions, domain modeling, and implementation details were reviewed, tested, and integrated by me.

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

Built with โค๏ธ for LoadUp Challenge

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages