{ "title": "CFA Study Kenya: How We Built SharkFlow's AI-Powered Exam Platform on African...
{"title":"CFA Study Kenya: How We Built SharkFlow's AI-Powered Exam Platform on African Infrastructure","content":"# CFA Study Kenya: How We Built SharkFlow's AI-Powered Exam Platform on African Infrastructure\n\nIf you're a developer in Africa working on fintech, you've probably hit the same wall we did: building for financial markets requires bulletproof infrastructure, but African network conditions aren't exactly forgiving. When SharkFlow set out to build a CFA exam preparation platform specifically for Kenya and the broader African market, we couldn't just copy-paste solutions from Silicon Valley.\n\nHere's how we engineered SharkFlow CFA from the ground upβand what we learned about building financial tech that actually works on 2G networks.\n\n## The Problem: Why Existing CFA Platforms Fail in Africa\n\nLet's be honest. The CFA Institute curriculum is dense. But that's not why African candidates struggleβthey struggle because every existing platform assumes you have unlimited bandwidth, low latency, and reliable electricity.\n\nKenny, a candidate from Nairobi, told us: *\"I pay Ksh 3,500/month for internet. I can't afford to re-download a 50MB video because I lost connection for 3 seconds.\"*\n\nThat statement shaped our entire architecture.\n\nCFA exam prep in Kenya alone represents a ~Ksh 500M+ market (rough estimate: 4,000 candidates Γ Ksh 125k average spend on prep materials). But we knew we'd only capture that market if we solved for:\n\n1. **Bandwidth scarcity** (many Kenyan users still on 3G/4G with data caps)\n2. **Intermittent connectivity** (Nairobi's power grid issues, rural network blackouts)\n3. **Payment fragmentation** (M-Pesa dominance, limited credit card adoption)\n4. **Offline-first requirements** (users need to study without data)\n\n## Architecture Decision 1: API-First, Async-Heavy Design\n\nWe went full-async. Here's why:\n\n```
javascript\n// Bad: Synchronous, assumes fast connection\napp.get('/api/study-material/:topicId', (req, res) => {\n const material = db.query(`SELECT * FROM materials WHERE id = ?`, req.params.topicId);\n const questions = db.query(`SELECT * FROM questions WHERE material_id = ?`, req.params.topicId);\n const explanations = db.query(`SELECT * FROM explanations WHERE material_id = ?`, req.params.topicId);\n res.json({ material, questions, explanations });\n});\n
```\n\nThis fails on slow networks. The user waits 8+ seconds, connection drops mid-request, nothing caches properly.\n\n```
javascript\n// Good: API endpoints designed for offline-first sync\napp.get('/api/sync/study-material/:topicId', async (req, res) => {\n try {\n const material = await getMaterial(req.params.topicId);\n const etag = generateETag(material);\n\n // Client sends If-None-Match header if it has cached version\n if (req.headers['if-none-match'] === etag) {\n return res.status(304).end();\n }\n\n // Compressed, chunked response\n res.set('ETag', etag);\n res.set('Cache-Control', 'public, max-age=604800'); // 1 week\n res.json({\n material,\n syncTimestamp: Date.now(),\n nextSyncWindow: calculateNextWindow()\n });\n } catch (err) {\n res.status(500).json({ error: err.message, offline: true });\n }\n});\n
```\n\nEach endpoint returns:\n- **ETag headers** for conditional requests (saves data)\n- **Cache-Control directives** (respects bandwidth)\n- **Sync timestamps** (the app knows what's fresh)\n- **Graceful degradation** (errors don't break offline experience)\n\n## Architecture Decision 2: Database Strategy for Scale\n\nWe chose **PostgreSQL for relational data + Redis for sync queues**. Here's the specific schema:\n\n```
sql\n-- Core study material (immutable, heavily cached)\nCREATE TABLE study_materials (\n id SERIAL PRIMARY KEY,\n topic_code VARCHAR(10) NOT NULL, -- e.g., 'L1_ETH_001'\n content JSONB NOT NULL, -- Stores curriculum section\n content_version INT DEFAULT 1,\n compressed_size INT,\n created_at TIMESTAMP DEFAULT NOW(),\n updated_at TIMESTAMP DEFAULT NOW(),\n is_active BOOLEAN DEFAULT TRUE\n);\n\nCREATE INDEX idx_topic_code ON study_materials(topic_code);\nCREATE INDEX idx_content_version ON study_materials(content_version);\n\n-- User progress (hot data, frequent writes)\nCREATE TABLE user_progress (\n id BIGSERIAL PRIMARY KEY,\n user_id BIGINT NOT NULL,\n material_id INT NOT NULL,\n last_accessed_at TIMESTAMP DEFAULT NOW(),\n completion_percentage NUMERIC(5,2),\n local_sync_token VARCHAR(64), -- UUID for offline sync\n FOREIGN KEY (material_id) REFERENCES study_materials(id)\n);\n\nCREATE INDEX idx_user_progress ON user_progress(user_id, last_accessed_at DESC);\n\n-- Exam mock questions (immutable, cached aggressively)\nCREATE TABLE mock_questions (\n id BIGSERIAL PRIMARY KEY,\n topic_id INT NOT NULL,\n question_text TEXT NOT NULL,\n options JSONB NOT NULL,\n correct_option INT NOT NULL,\n explanation TEXT,\n created_at TIMESTAMP DEFAULT NOW(),\n FOREIGN KEY (topic_id) REFERENCES study_materials(id)\