MGSLG Analytics Platform - Architecture Decision Records (ADR)
MGSLG Analytics Platform - Architecture Decision Records (ADR)
Section titled “MGSLG Analytics Platform - Architecture Decision Records (ADR)”Version: 1.0 Last Updated: October 2025
Table of Contents
Section titled “Table of Contents”- ADR-001: FastAPI over Django/Flask
- ADR-002: Railway over AWS/Heroku/Azure
- ADR-003: Next.js over React/Vue/Angular
- ADR-004: PostgreSQL over MySQL/MongoDB
- ADR-005: Async SQLAlchemy 2.0
- ADR-006: Session-Based Authentication
- ADR-007: Tailwind CSS over Material-UI/Bootstrap
- ADR-008: Recharts over Chart.js/D3.js
- ADR-009: Monorepo Structure
- ADR-010: District-Level Geographic Tracking
- ADR-011: SACE Points 3-Year Cycle Design
- ADR-012: ML Scoring Algorithm (Not Neural Networks)
ADR-001: FastAPI over Django/Flask
Section titled “ADR-001: FastAPI over Django/Flask”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need to choose a Python web framework for building RESTful API backend.
Options Considered
Section titled “Options Considered”- Django + Django REST Framework (DRF)
- Flask + Flask-RESTful
- FastAPI ✅
Decision
Section titled “Decision”Chosen: FastAPI
Rationale
Section titled “Rationale”Why FastAPI:
- ✅ Performance: ASGI-based, significantly faster than Django/Flask (3-4x faster)
- ✅ Async Support: Native async/await for database operations
- ✅ Auto Documentation: Built-in Swagger/OpenAPI docs at
/docs - ✅ Type Safety: Pydantic models for request/response validation
- ✅ Modern Python: Uses Python 3.11+ features (type hints, async)
- ✅ Developer Experience: Auto-completion, fewer bugs, faster development
Why Not Django:
- ❌ Heavy framework with ORM we don’t need (using SQLAlchemy)
- ❌ Slower WSGI-based (not async)
- ❌ More boilerplate for simple API
- ❌ Admin panel unnecessary for this project
Why Not Flask:
- ❌ No built-in async support (need workarounds)
- ❌ Manual API documentation
- ❌ Less modern (no Pydantic validation)
- ❌ More manual work for request validation
Consequences
Section titled “Consequences”Positive:
- API responds in <200ms (target met)
- Auto-generated API docs save documentation time
- Type hints catch bugs during development
- Easy to write async database queries
Negative:
- Smaller ecosystem than Django/Flask
- Team needs to learn FastAPI (learning curve ~2 days)
Mitigation:
- FastAPI documentation is excellent
- Community growing rapidly
- Active development and support
ADR-002: Railway over AWS/Heroku/Azure
Section titled “ADR-002: Railway over AWS/Heroku/Azure”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need cloud hosting platform for production deployment with CI/CD.
Options Considered
Section titled “Options Considered”- AWS (Elastic Beanstalk + RDS)
- Heroku
- Azure App Service
- Railway ✅
- DigitalOcean App Platform
Decision
Section titled “Decision”Chosen: Railway
Rationale
Section titled “Rationale”Why Railway:
- ✅ Simplicity: Zero-config deployment from GitHub
- ✅ Cost-Effective: $5-20/month for MVP (AWS would be $50+)
- ✅ Built-in PostgreSQL: Managed database included
- ✅ Auto-Deploy: Push to GitHub → Auto-deploy (no manual steps)
- ✅ Environment Variables: Easy management
- ✅ Logs & Monitoring: Built-in dashboard
- ✅ No DevOps Required: Perfect for small team
Why Not AWS:
- ❌ Complex setup (VPC, security groups, load balancer, RDS)
- ❌ Higher cost for small projects
- ❌ Requires DevOps expertise
- ❌ Over-engineering for demo/MVP
Why Not Heroku:
- ❌ Expensive ($25+ for Postgres alone)
- ❌ Sleep mode on free tier (not acceptable for demo)
- ❌ Postgres row limits on free tier
Why Not Azure:
- ❌ Complex pricing structure
- ❌ Steeper learning curve
- ❌ Overkill for project size
Why Not DigitalOcean:
- ⚠️ Good alternative, but Railway has better GitHub integration
- ⚠️ Requires more manual configuration
Consequences
Section titled “Consequences”Positive:
- Deployed production in <30 minutes
- Zero downtime deployments
- Automatic HTTPS certificates
- Team can focus on features, not infrastructure
Negative:
- Vendor lock-in (migration effort if needed)
- Less control over infrastructure
- Limited to Railway’s available regions
Mitigation:
- Keep Docker configs for portability
- Document deployment process for migration if needed
- Railway’s pricing is stable and affordable
ADR-003: Next.js over React/Vue/Angular
Section titled “ADR-003: Next.js over React/Vue/Angular”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need frontend framework for building interactive analytics dashboards.
Options Considered
Section titled “Options Considered”- React (CRA/Vite)
- Vue.js 3
- Angular
- Next.js 14 ✅
Decision
Section titled “Decision”Chosen: Next.js 14
Rationale
Section titled “Rationale”Why Next.js:
- ✅ React-Based: Familiar React ecosystem
- ✅ Server-Side Rendering (SSR): Better SEO and performance
- ✅ File-Based Routing: No manual route configuration
- ✅ API Routes: Can build backend routes if needed
- ✅ Built-in Optimization: Image optimization, code splitting, lazy loading
- ✅ TypeScript Support: First-class TypeScript integration
- ✅ Fast Refresh: Instant feedback during development
- ✅ Production-Ready: Used by Netflix, Uber, Nike
Why Not React (CRA/Vite):
- ❌ Manual routing setup
- ❌ No SSR out of the box
- ❌ More configuration required
- ❌ Less opinionated (more decisions to make)
Why Not Vue.js:
- ❌ Smaller ecosystem than React
- ❌ Less enterprise adoption
- ❌ Team familiarity with React preferred
Why Not Angular:
- ❌ Steeper learning curve
- ❌ More boilerplate
- ❌ TypeScript required (not optional)
- ❌ Heavier framework (slower initial load)
Consequences
Section titled “Consequences”Positive:
- Dashboard loads in <2 seconds
- SEO-friendly (for public pages)
- Developer experience is excellent
- Easy to add new pages (just create file)
Negative:
- Next.js 14 is newer (some features in beta)
- Server components can be confusing initially
Mitigation:
- Use stable features for production
- Document Next.js patterns for team
- Leverage community resources
ADR-004: PostgreSQL over MySQL/MongoDB
Section titled “ADR-004: PostgreSQL over MySQL/MongoDB”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need database for storing participant, program, enrollment, and analytics data.
Options Considered
Section titled “Options Considered”- MySQL
- PostgreSQL ✅
- MongoDB
- SQLite
Decision
Section titled “Decision”Chosen: PostgreSQL 15
Rationale
Section titled “Rationale”Why PostgreSQL:
- ✅ ACID Compliance: Strong data integrity for financial/compliance data
- ✅ JSON Support: Can store flexible data without schema changes
- ✅ Advanced Queries: Window functions, CTEs, array operations
- ✅ Full-Text Search: Built-in search capabilities
- ✅ Scalability: Handles millions of rows efficiently
- ✅ Open Source: No licensing costs
- ✅ Railway Support: Fully managed Postgres on Railway
Why Not MySQL:
- ❌ Less advanced features (no window functions in older versions)
- ❌ JSON support not as robust
- ❌ Weaker for complex analytics queries
Why Not MongoDB:
- ❌ NoSQL overkill for structured data
- ❌ Lacks ACID transactions (critical for enrollments/payments)
- ❌ More complex aggregations for analytics
- ❌ No foreign key constraints (data integrity risk)
Why Not SQLite:
- ❌ Not suitable for production multi-user apps
- ❌ No concurrent writes
- ❌ Limited scalability
Consequences
Section titled “Consequences”Positive:
- Complex analytics queries run efficiently
- Data integrity guaranteed (foreign keys, constraints)
- JSON fields allow flexible SACE metadata
- Easy to backup and restore
Negative:
- Slightly more complex than MySQL (advanced features)
- Team needs to learn Postgres-specific features
Mitigation:
- Use SQLAlchemy ORM (abstracts DB differences)
- Document Postgres-specific queries
- Leverage Railway’s managed Postgres (no server maintenance)
ADR-005: Async SQLAlchemy 2.0
Section titled “ADR-005: Async SQLAlchemy 2.0”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need ORM for database operations with FastAPI’s async capabilities.
Options Considered
Section titled “Options Considered”- SQLAlchemy 1.4 (Sync)
- SQLAlchemy 2.0 (Async) ✅
- Tortoise ORM
- Peewee
Decision
Section titled “Decision”Chosen: SQLAlchemy 2.0 with asyncpg driver
Rationale
Section titled “Rationale”Why Async SQLAlchemy 2.0:
- ✅ Performance: Non-blocking database queries (handle 100+ concurrent requests)
- ✅ FastAPI Integration: Works perfectly with FastAPI’s async
- ✅ Modern API: Cleaner syntax than 1.4
- ✅ Type Hints: Better IDE support
- ✅ Mature: SQLAlchemy is industry standard (15+ years)
- ✅ Asyncpg Driver: Fastest Postgres driver for Python
Why Not SQLAlchemy 1.4 (Sync):
- ❌ Blocks event loop (slows down API)
- ❌ Can’t handle concurrent requests efficiently
- ❌ Older API design
Why Not Tortoise ORM:
- ❌ Smaller community
- ❌ Less mature (bugs more likely)
- ❌ Limited migration tools
Why Not Peewee:
- ❌ No async support
- ❌ Less powerful than SQLAlchemy
- ❌ Smaller ecosystem
Consequences
Section titled “Consequences”Positive:
- API can handle 50+ concurrent users
- Database queries don’t block other requests
- Response times stay <200ms under load
Negative:
- Async code slightly more complex (
awaiteverywhere) - Debugging async can be tricky
Mitigation:
- Use async best practices (no blocking operations)
- Add logging for async operations
- Test with concurrent requests
Code Example
Section titled “Code Example”# Async query (non-blocking)async def get_participants(db: AsyncSession, province: str): query = select(Participant).where(Participant.province == province) result = await db.execute(query) return result.scalars().all()ADR-006: Session-Based Authentication
Section titled “ADR-006: Session-Based Authentication”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need authentication for demo platform (not production-grade yet).
Options Considered
Section titled “Options Considered”- JWT (JSON Web Tokens)
- OAuth 2.0 (Google/Microsoft)
- Session-Based (Cookies) ✅
- API Keys
Decision
Section titled “Decision”Chosen: Session-Based Authentication with Cookies
Rationale
Section titled “Rationale”Why Session-Based:
- ✅ Simple for Demo: Single password (
mgslg2025) - ✅ Stateful: Easy to logout and revoke access
- ✅ No Token Management: No refresh tokens, no expiry logic
- ✅ Secure Cookies: httpOnly, secure flags prevent XSS
- ✅ Fast to Implement: <1 day development time
Why Not JWT:
- ❌ Overkill for single-user demo
- ❌ Can’t revoke tokens easily (need Redis/DB)
- ❌ More complex logout flow
- ❌ Token expiry management needed
Why Not OAuth 2.0:
- ❌ Way too complex for demo
- ❌ Requires external provider setup
- ❌ Not needed (no user accounts yet)
Why Not API Keys:
- ❌ No UI-friendly (need to manage keys)
- ❌ Less secure for browser apps
Consequences
Section titled “Consequences”Positive:
- Login works in 10 seconds
- Logout is instant
- No token storage in browser
- Simple codebase
Negative:
- Not suitable for multi-tenant production
- Sessions stored in memory (lost on server restart)
Mitigation:
- Phase 2: Implement JWT + Redis for production
- Document upgrade path to proper auth
- For demo, current solution is perfect
Future Upgrade Path
Section titled “Future Upgrade Path”# Phase 2: Add JWT authenticationfrom fastapi_jwt_auth import AuthJWT
@router.post("/login")async def login(credentials: LoginCredentials, Authorize: AuthJWT = Depends()): # Verify credentials access_token = Authorize.create_access_token(subject=user.email) return {"access_token": access_token}ADR-007: Tailwind CSS over Material-UI/Bootstrap
Section titled “ADR-007: Tailwind CSS over Material-UI/Bootstrap”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need CSS framework for building responsive dashboard UI.
Options Considered
Section titled “Options Considered”- Material-UI (MUI)
- Bootstrap
- Ant Design
- Tailwind CSS ✅
Decision
Section titled “Decision”Chosen: Tailwind CSS
Rationale
Section titled “Rationale”Why Tailwind CSS:
- ✅ Utility-First: Rapid prototyping with utility classes
- ✅ Customizable: Easy to match MGSLG branding
- ✅ Small Bundle Size: Only includes used classes (PurgeCSS)
- ✅ No JavaScript: Pure CSS (faster, simpler)
- ✅ Responsive: Built-in breakpoints (sm, md, lg, xl)
- ✅ Modern: Used by GitHub, Netflix, NASA
Why Not Material-UI:
- ❌ Heavier bundle size (~200KB+)
- ❌ Harder to customize (theme overrides complex)
- ❌ Components have opinions (we want custom design)
- ❌ JavaScript-based (adds complexity)
Why Not Bootstrap:
- ❌ Outdated design patterns
- ❌ Too opinionated (hard to make unique)
- ❌ jQuery dependency (not needed)
Why Not Ant Design:
- ❌ Very opinionated (Chinese design language)
- ❌ Large bundle size
- ❌ Harder to customize than Tailwind
Consequences
Section titled “Consequences”Positive:
- Custom MGSLG-branded UI in 2 days
- Bundle size: 180KB gzipped (very small)
- Easy to make responsive changes
- No CSS file conflicts
Negative:
- HTML can look cluttered with many classes
- Learning curve for utility-first approach
Mitigation:
- Extract common patterns to components
- Use
@applydirective for repeated patterns - Document design system in Storybook (future)
Example
Section titled “Example”// Tailwind approach<div className="bg-blue-500 text-white p-4 rounded-lg shadow-md hover:bg-blue-600 transition"> <h2 className="text-2xl font-bold">KPI Card</h2></div>ADR-008: Recharts over Chart.js/D3.js
Section titled “ADR-008: Recharts over Chart.js/D3.js”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need charting library for dashboard visualizations (line charts, bar charts, pie charts).
Options Considered
Section titled “Options Considered”- Chart.js
- D3.js
- Recharts ✅
- Victory Charts
- Nivo
Decision
Section titled “Decision”Chosen: Recharts
Rationale
Section titled “Rationale”Why Recharts:
- ✅ React-Native: Built for React (declarative JSX)
- ✅ Responsive: Auto-resizes with container
- ✅ TypeScript Support: Full type definitions
- ✅ Composable: Build complex charts from primitives
- ✅ Lightweight: ~50KB gzipped
- ✅ Built on D3: Uses D3 for calculations (best of both worlds)
Why Not Chart.js:
- ❌ Imperative API (not React-friendly)
- ❌ Manual React integration (need react-chartjs-2 wrapper)
- ❌ Less flexible for custom interactions
Why Not D3.js:
- ❌ Very low-level (takes weeks to build charts)
- ❌ Steep learning curve
- ❌ Not React-friendly (manipulates DOM directly)
- ❌ Overkill for standard charts
Why Not Victory:
- ⚠️ Good alternative, but larger bundle size
- ⚠️ Less active community than Recharts
Why Not Nivo:
- ⚠️ Great for complex visualizations, but heavier
- ⚠️ More opinionated (harder to customize)
Consequences
Section titled “Consequences”Positive:
- Charts built in hours, not days
- Easy to add tooltips, legends, animations
- Responsive out of the box
- TypeScript errors caught early
Negative:
- Limited to chart types Recharts supports
- Custom interactions require workarounds
Mitigation:
- Use D3 for one-off custom visualizations (if needed)
- Recharts covers 95% of use cases
- Community has solutions for edge cases
Example
Section titled “Example”<ResponsiveContainer width="100%" height={300}> <LineChart data={enrollmentTrends}> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="month" /> <YAxis /> <Tooltip /> <Line type="monotone" dataKey="enrollments" stroke="#3b82f6" /> </LineChart></ResponsiveContainer>ADR-009: Monorepo Structure
Section titled “ADR-009: Monorepo Structure”Status: ✅ Accepted Date: September 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need to organize backend and frontend in same repository.
Options Considered
Section titled “Options Considered”- Separate Repos (backend + frontend)
- Monorepo (single repo) ✅
Decision
Section titled “Decision”Chosen: Monorepo with /backend and /frontend directories
Rationale
Section titled “Rationale”Why Monorepo:
- ✅ Simplicity: One repo to clone, one place for docs
- ✅ Shared Docs: Documentation applies to both backend/frontend
- ✅ Atomic Commits: Backend + frontend changes in one commit
- ✅ Easier Onboarding: New devs clone one repo
- ✅ Shared Scripts:
start_local.shruns both services
Why Not Separate Repos:
- ❌ Two repos to manage (more complexity)
- ❌ Documentation scattered
- ❌ Cross-repo changes require multiple PRs
- ❌ Harder to keep in sync
Structure
Section titled “Structure”mathew-goniwe/├── backend/ # FastAPI + Python├── frontend/ # Next.js + TypeScript├── docs/ # Shared documentation├── start_local.sh # Unified startup script└── README.md # Project overviewConsequences
Section titled “Consequences”Positive:
- Single source of truth
- Documentation colocated with code
- Easy to run full stack locally
Negative:
- Larger repo size
- CI/CD needs to detect which service changed
Mitigation:
- Railway deploys backend and frontend separately
- Git tags can differentiate versions
.gitignorekeeps repo size manageable
ADR-010: District-Level Geographic Tracking
Section titled “ADR-010: District-Level Geographic Tracking”Status: ✅ Accepted Date: October 2024 Decision Makers: Technical Lead, MGSLG Stakeholders
Context
Section titled “Context”Need to track participants at district level (not just province) for Gauteng.
Options Considered
Section titled “Options Considered”- Province-Only Tracking
- District + Province (Nullable District) ✅
- Separate District Table with Foreign Key
- Full GIS with Coordinates
Decision
Section titled “Decision”Chosen: Add nullable district field to Participant model
Rationale
Section titled “Rationale”Why Nullable District Field:
- ✅ Simple Schema: One field, no joins needed for basic queries
- ✅ Backward Compatible: Existing participants (no district) still work
- ✅ Fast Queries: Indexed field, no join overhead
- ✅ Flexible: Can add districts to other provinces later
- ✅ Easy Filtering:
WHERE province = 'Gauteng' AND district = 'Johannesburg Central'
Why Not Province-Only:
- ❌ Not granular enough for MGSLG needs
- ❌ Can’t analyze district-level performance
Why Not Separate District Table:
- ❌ Over-engineering for 15 districts
- ❌ JOIN overhead on every query
- ❌ More complex migrations
Why Not Full GIS:
- ❌ Massive overkill (we don’t need maps)
- ❌ Complex queries (PostGIS extension)
- ❌ No business requirement for coordinates
Consequences
Section titled “Consequences”Positive:
- 15 Gauteng districts configured in 1 hour
- Queries stay fast (<200ms)
- Easy to add districts for other provinces
Negative:
- No referential integrity (districts are strings, not FKs)
- Typos possible (mitigated with validation)
Mitigation:
- Frontend dropdown ensures valid districts
- Backend validation against whitelist
- Future: Add District reference table if needed
District List (Gauteng - 15 districts)
Section titled “District List (Gauteng - 15 districts)”GAUTENG_DISTRICTS = [ "Johannesburg Central", "Johannesburg East", "Johannesburg North", "Johannesburg South", "Johannesburg West", "Ekurhuleni North", "Ekurhuleni South", "Tshwane North", "Tshwane South", "Tshwane West", "Gauteng East", "Gauteng North", "Gauteng West", "Sedibeng East", "Sedibeng West"]ADR-011: SACE Points 3-Year Cycle Design
Section titled “ADR-011: SACE Points 3-Year Cycle Design”Status: ✅ Accepted Date: October 2024 Decision Makers: Technical Lead, SACE Compliance Expert
Context
Section titled “Context”SACE requires teachers to earn 150 CPTD points over 3 years. Need to track both current cycle and lifetime points.
Options Considered
Section titled “Options Considered”- Lifetime Points Only
- Current Cycle Points Only
- Dual Tracking (Current + Lifetime) ✅
- Point History Table with Cycles
Decision
Section titled “Decision”Chosen: Dual tracking with cycle dates in participant_sace_points table
Rationale
Section titled “Rationale”Why Dual Tracking:
- ✅ Compliance Reporting: Shows current cycle progress (0-150)
- ✅ Historical Record: Lifetime total for recognition/awards
- ✅ Risk Assessment: Compare current cycle to expected (50 pts/year)
- ✅ Simple Queries: No complex aggregations needed
- ✅ Accurate Status: “On Track” vs “At Risk” calculated from cycle dates
Why Not Lifetime Only:
- ❌ Can’t determine if participant is compliant for current cycle
- ❌ No way to identify at-risk participants
Why Not Current Cycle Only:
- ❌ Lose historical achievement data
- ❌ Can’t recognize long-term participants
Why Not Point History Table:
- ❌ Over-engineering (we have enrollments table for history)
- ❌ Complex aggregations on every query
- ❌ Slower performance
Schema Design
Section titled “Schema Design”class ParticipantSACEPoints(Base): total_points_earned: int # Lifetime (e.g., 245) points_current_cycle: int # Capped at 150 per cycle cycle_start_date: datetime # First SACE enrollment date cycle_end_date: datetime # cycle_start + 3 years compliance_status: str # on_track, at_risk, behindBusiness Rules
Section titled “Business Rules”- Cycle Start: First SACE-earning enrollment date
- Cycle Duration: Exactly 3 years (1,095 days)
- Cycle Cap: Maximum 150 points per cycle (excess rolls to next)
- Expected Rate: 50 points per year (linear trajectory)
- Compliance Status:
- On Track: Current >= Expected
- At Risk: Current >= 70% of Expected
- Behind: Current < 70% of Expected
Consequences
Section titled “Consequences”Positive:
- Clear compliance reporting (meets SACE requirements)
- Easy to identify at-risk participants
- Fast queries (no aggregations needed)
Negative:
- Must update cycle dates when new cycle starts
- Edge case: What if participant skips a cycle?
Mitigation:
- Cron job to update cycle dates annually
- Handle skipped cycles in business logic (future)
- Document cycle rollover process
Example Calculation
Section titled “Example Calculation”# Participant with 2.5 years in current cyclecycle_start = "2022-06-01"current_date = "2025-01-01"years_elapsed = 2.5
# Expected points: 50 points/yearexpected_points = 2.5 * 50 = 125
# Actual points: 95current_cycle_points = 95
# Status: 95 < 125 but 95 >= (125 * 0.7)# Result: "at_risk"ADR-012: ML Scoring Algorithm (Not Neural Networks)
Section titled “ADR-012: ML Scoring Algorithm (Not Neural Networks)”Status: ✅ Accepted Date: October 2024 Decision Makers: Technical Lead
Context
Section titled “Context”Need to predict career progression likelihood for participants.
Options Considered
Section titled “Options Considered”- Rule-Based Scoring (If-Else Logic)
- Weighted Scoring Algorithm ✅
- Logistic Regression (scikit-learn)
- Neural Networks (TensorFlow/PyTorch)
- Random Forest
Decision
Section titled “Decision”Chosen: Weighted Scoring Algorithm (custom formula)
Rationale
Section titled “Rationale”Why Weighted Scoring:
- ✅ Transparent: Stakeholders understand the logic
- ✅ Explainable: Can show why score is High/Medium/Low
- ✅ No Training Data Needed: Works with current dataset
- ✅ Deterministic: Same input = same output (reproducible)
- ✅ Fast: Calculates in <10ms per participant
- ✅ Customizable: Easy to adjust weights based on feedback
Why Not Rule-Based (If-Else):
- ❌ Too rigid (can’t handle nuances)
- ❌ Hard to maintain (many branches)
Why Not Logistic Regression:
- ❌ Needs labeled training data (we don’t have “promoted” labels yet)
- ❌ Less interpretable than scoring
- ❌ Overkill for MVP
Why Not Neural Networks:
- ❌ Extreme overkill (need 10,000+ samples)
- ❌ Black box (stakeholders won’t trust it)
- ❌ Slow training and inference
- ❌ Requires GPUs for reasonable performance
Why Not Random Forest:
- ⚠️ Good middle ground, but needs labeled data
- ⚠️ Less interpretable than scoring
Scoring Formula
Section titled “Scoring Formula”progression_score = ( participation_weight * 0.30 + # Programs completed (0-30) completion_weight * 0.20 + # Completion rate (0-20) experience_weight * 0.15 + # Years of experience (0-15) performance_weight * 0.35 # Avg feedback rating (0-35))# Total: 0-100
# Likelihood classification:# 80-100: High (6-12 months to promotion)# 60-79: Medium (12-18 months)# 0-59: Low (18+ months)Consequences
Section titled “Consequences”Positive:
- Stakeholders trust the predictions (can explain)
- No training phase (works immediately)
- Easy to tune based on real outcomes
- Faster than ML models
Negative:
- Not as accurate as trained ML (when we have data)
- Weights are somewhat arbitrary (educated guesses)
Mitigation:
- Phase 2: Collect promotion outcome data
- Phase 3: Train logistic regression with real labels
- Phase 4: Compare scoring vs ML accuracy
- Adjust weights based on real-world feedback
Future ML Upgrade Path
Section titled “Future ML Upgrade Path”# Phase 3: When we have labeled datafrom sklearn.linear_model import LogisticRegression
X = [participation, completion, experience, performance]y = [1 if promoted else 0] # Labels from career_updates table
model = LogisticRegression()model.fit(X, y)
# Predict probability of promotionpromotion_probability = model.predict_proba(X_new)[0][1]Summary of Key Decisions
Section titled “Summary of Key Decisions”| ADR | Decision | Why? |
|---|---|---|
| 001 | FastAPI | Performance, async, auto-docs |
| 002 | Railway | Simplicity, cost, auto-deploy |
| 003 | Next.js | SSR, routing, React ecosystem |
| 004 | PostgreSQL | ACID, JSON, analytics queries |
| 005 | Async SQLAlchemy | Non-blocking, concurrency |
| 006 | Session Auth | Simple for demo, upgrade later |
| 007 | Tailwind CSS | Utility-first, customizable, small bundle |
| 008 | Recharts | React-native, declarative, easy |
| 009 | Monorepo | Single source of truth, shared docs |
| 010 | District Field | Simple, fast, backward-compatible |
| 011 | Dual SACE Tracking | Compliance + history |
| 012 | Scoring Algorithm | Explainable, fast, no training needed |
Revision History
Section titled “Revision History”| Version | Date | Changes |
|---|---|---|
| 1.0 | Oct 2025 | Initial ADRs (001-012) |
Architecture decisions evolve. Update this document when technical choices change.