Hearts in Scrubs is South Africa's premier healthcare staffing platform, connecting qualified medical professionals with healthcare facilities in need. The platform processes thousands of shift assignments monthly, managing complex scheduling, credentialing, and real-time communication.
This case study explores the technical architecture that enables Hearts in Scrubs to operate at scale while maintaining code quality and development velocity through LEGO Builder principles.
The Challenge
Healthcare staffing involves unique challenges that most platforms don't face:
- Credential Verification: Medical professionals must have valid licenses, certifications, and background checks
- Complex Matching: Facilities need specific qualifications, experience levels, and availability patterns
- Real-Time Scheduling: Last-minute cancellations and urgent shift coverage require instant notifications
- Compliance Requirements: POPIA (South Africa's data protection law) and healthcare regulations
- Payment Processing: Multi-party transactions with facilities, professionals, and platform fees
Technical Architecture
Database Schema
The platform uses PostgreSQL with a normalized schema optimized for complex queries:
// Database models follow single-responsibility principle
// models/professional.js (27 lines)
export const ProfessionalModel = {
tableName: 'professionals',
fields: {
id: 'uuid',
userId: 'uuid',
profession: 'string', // Nurse, Doctor, Paramedic
experienceYears: 'integer',
licenses: 'jsonb', // Array of license objects
availability: 'jsonb', // Weekly availability pattern
rating: 'decimal',
verificationStatus: 'enum'
}
};
Matching Algorithm
The heart of the platform is the matching algorithm that pairs professionals with shifts:
// controllers/shift-matcher.js (29 lines)
export async function findMatchingProfessionals(shift) {
const candidates = await getProfessionalsInRadius(
shift.facilityLocation,
shift.radiusKm
);
const qualified = filterByQualifications(candidates, shift.requirements);
const available = filterByAvailability(qualified, shift.startTime, shift.endTime);
const scored = scoreMatches(available, shift);
return scored.sort((a, b) => b.score - a.score);
}
// utilities/match-scorer.js (24 lines)
export function scoreMatches(professionals, shift) {
return professionals.map(pro => ({
...pro,
score: calculateMatchScore(pro, shift)
}));
}
function calculateMatchScore(professional, shift) {
let score = 0;
score += professional.rating * 10;
score += professional.completedShifts * 0.5;
score += getExperienceBonus(professional.experienceYears);
score += getDistanceBonus(professional.location, shift.facilityLocation);
return score;
}
Real-Time Notifications
The platform uses WebSockets for instant updates when shifts become available or are filled:
// bridges/notification-bridge.js (28 lines)
export async function notifyMatchingProfessionals(shift, professionals) {
const notifications = professionals.map(pro => ({
userId: pro.userId,
type: 'shift_available',
data: {
shiftId: shift.id,
facility: shift.facilityName,
startTime: shift.startTime,
rate: shift.hourlyRate
}
}));
await Promise.all([
sendPushNotifications(notifications),
sendEmailNotifications(notifications),
broadcastWebSocket(notifications)
]);
}
LEGO Builder Implementation
Block Organization
The codebase is organized into clear categories:
src/
├── utilities/ // Pure functions (date formatting, scoring, validation)
├── builders/ // Object constructors (shift builder, notification builder)
├── guards/ // Auth and authorization checks
├── processors/ // Business logic (payment processing, credential verification)
├── bridges/ // External service integrations (Twilio, SendGrid, Stripe)
└── controllers/ // Request handlers and orchestration
Example: Credential Verification Flow
Here's how LEGO blocks compose to verify a professional's credentials:
// controllers/credential-controller.js (26 lines)
export async function verifyProfessionalCredentials(professionalId) {
const professional = await getProfessional(professionalId);
const licenseValid = await verifyLicense(professional.licenseNumber);
const backgroundClear = await checkBackgroundStatus(professionalId);
const certificatesValid = verifyCertificates(professional.certificates);
const verificationResult = {
licenseValid,
backgroundClear,
certificatesValid,
overallStatus: licenseValid && backgroundClear && certificatesValid
};
await updateVerificationStatus(professionalId, verificationResult);
await notifyProfessional(professionalId, verificationResult);
return verificationResult;
}
// Each function is a separate <30 line block
Performance Optimizations
Query Optimization
// utilities/geo-query-builder.js (22 lines)
export function buildRadiusQuery(center, radiusKm) {
// Use PostGIS for efficient geographic queries
return `
SELECT *
FROM professionals
WHERE ST_DWithin(
location::geography,
ST_MakePoint(${center.lng}, ${center.lat})::geography,
${radiusKm * 1000}
)
AND verification_status = 'approved'
AND availability IS NOT NULL
`;
}
Caching Strategy
Frequently accessed data is cached to reduce database load:
- Professional profiles: 5-minute cache with automatic invalidation on updates
- Facility details: 1-hour cache
- Availability patterns: Real-time, no cache
- Match scores: Computed on-demand, cached for 1 minute
Results and Impact
The LEGO Builder approach delivered measurable results:
- Development Speed: New features ship 40% faster than previous monolithic approach
- Bug Resolution: Average bug fix time reduced from 4 hours to 45 minutes
- Code Quality: Test coverage increased to 87% (from 43%)
- Onboarding Time: New developers contribute production code within 1 week (previously 3 weeks)
- Platform Performance: Average API response time: 120ms, 99.8% uptime
Lessons Learned
1. Start With Utilities
Pure utility functions are the easiest blocks to extract and provide immediate value. They're also the most reusable across projects.
2. Controllers Should Orchestrate, Not Implement
Controllers that just call other blocks (no implementation logic) are easier to understand and test. They read like a recipe.
3. Break When Blocks Get Too Big
When a block approaches 30 lines, it's time to extract helper functions. Don't wait until it's already over the limit.
4. Document Block Contracts
Clear input/output documentation makes blocks easier to reuse and prevents misuse. TypeScript helps enforce this at compile time.
Conclusion
Hearts in Scrubs demonstrates that LEGO Builder architecture scales from small utilities to complex, multi-faceted platforms. The key is consistent application of the principles: atomic blocks, single responsibility, and clear boundaries.
By treating every piece of code as a reusable LEGO block, we built a platform that's fast to develop, easy to maintain, and straightforward to scale.
← Back to Blog