Skip to main content

02 — Low-Level Design (LLD)

Dokumen ini menjelaskan detail implementasi setiap modul — struktur folder, class/function design, dan sequence diagram untuk alur-alur kritis.


Struktur Folder Project​

apps/app-be/
├── src/
│ ├── index.ts # Entry point — start Hono server
│ ├── app.ts # Hono app instance + global middleware
│ ├── env.ts # Environment variable validation (Zod)
│ │
│ ├── middleware/ # Custom middleware
│ │ ├── auth.ts # Auth guard (require session)
│ │ ├── rate-limit.ts # Rate limiting per route
│ │ ├── logger.ts # Request/response logging (Pino)
│ │ └── error-handler.ts # Global error handler
│ │
│ ├── routes/ # Route definitions (thin controllers)
│ │ ├── auth.routes.ts # /api/v1/auth/*
│ │ ├── user.routes.ts # /api/v1/users/*
│ │ ├── assessment.routes.ts # /api/v1/assessments/*
│ │ ├── learning.routes.ts # /api/v1/learning/*
│ │ ├── conversation.routes.ts # /api/v1/conversations/*
│ │ ├── content.routes.ts # /api/v1/content/*
│ │ ├── membership.routes.ts # /api/v1/membership/*
│ │ └── admin.routes.ts # /api/v1/admin/*
│ │
│ ├── services/ # Business logic layer
│ │ ├── auth.service.ts
│ │ ├── user.service.ts
│ │ ├── assessment.service.ts
│ │ ├── learning.service.ts
│ │ ├── conversation.service.ts
│ │ ├── content.service.ts
│ │ ├── membership.service.ts
│ │ └── admin.service.ts
│ │
│ ├── ai/ # AI-specific logic
│ │ ├── orchestrator.ts # AI call orchestration
│ │ ├── prompts/ # Prompt templates
│ │ │ ├── companion.ts # AI Companion system prompt
│ │ │ ├── assessment.ts # Assessment evaluation prompts
│ │ │ ├── feedback.ts # Speaking feedback prompts
│ │ │ └── blueprint.ts # Learning blueprint generation
│ │ ├── stt.ts # Speech-to-Text wrapper (Deepgram)
│ │ ├── tts.ts # Text-to-Speech wrapper (OpenAI)
│ │ └── pronunciation.ts # Pronunciation analysis logic
│ │
│ ├── db/ # Database layer
│ │ ├── index.ts # Drizzle client instance
│ │ ├── schema/ # Table definitions
│ │ │ ├── auth.schema.ts # users, sessions, accounts
│ │ │ ├── assessment.schema.ts
│ │ │ ├── learning.schema.ts
│ │ │ ├── conversation.schema.ts
│ │ │ ├── content.schema.ts
│ │ │ ├── membership.schema.ts
│ │ │ └── index.ts # Export semua schema
│ │ ├── migrations/ # SQL migration files (auto-generated)
│ │ └── seed/ # Seed data
│ │ ├── content.seed.ts # Domain, topic, content awal
│ │ └── plans.seed.ts # Subscription plans
│ │
│ ├── queue/ # Background job definitions
│ │ ├── worker.ts # BullMQ worker entry point
│ │ ├── queues.ts # Queue instances
│ │ └── jobs/
│ │ ├── process-audio.job.ts
│ │ ├── evaluate-speech.job.ts
│ │ ├── update-progress.job.ts
│ │ ├── send-email.job.ts
│ │ └── cleanup.job.ts
│ │
│ ├── lib/ # Shared utilities
│ │ ├── redis.ts # Redis client instance
│ │ ├── storage.ts # S3/R2 client instance
│ │ ├── errors.ts # Custom error classes
│ │ ├── constants.ts # App-wide constants
│ │ └── utils.ts # Helper functions
│ │
│ └── types/ # Shared TypeScript types
│ ├── api.types.ts # Request/Response types
│ ├── ai.types.ts # AI-related types
│ └── domain.types.ts # Business domain types
│
├── drizzle.config.ts # Drizzle Kit configuration
├── package.json
├── tsconfig.json
└── vitest.config.ts

Detail Modul​

Module 1: Auth Module​

Tanggung jawab: Registrasi, login, logout, session management, OAuth.

┌──────────────────────────────────────────────────┐
│ AUTH MODULE │
│ │
│ Routes (auth.routes.ts) │
│ ├── POST /auth/sign-up/email → Register │
│ ├── POST /auth/sign-in/email → Login │
│ ├── POST /auth/sign-in/social → OAuth │
│ ├── POST /auth/sign-out → Logout │
│ ├── GET /auth/session → Get Session │
│ └── POST /auth/forgot-password → Reset PW │
│ │
│ Service (auth.service.ts) │
│ ├── createUser(data) → User │
│ ├── linkGuestData(guestId, userId)→ void │
│ └── deleteAccount(userId) → void │
│ │
│ Better Auth handles: │
│ ├── Password hashing (Argon2) │
│ ├── Session token generation │
│ ├── OAuth flow (Google, Apple) │
│ ├── Email verification │
│ └── Session storage (PostgreSQL via Drizzle) │
└──────────────────────────────────────────────────┘

Sequence: Guest → Register → Link Data

Guest User Frontend API Server Database
│ │ │ │
│ (sudah punya │ │ │
│ assessment data │ │ │
│ di localStorage) │ │ │
│ │ │ │
│ ── Klik Register ─►│ │ │
│ │ ── POST /auth/ │ │
│ │ sign-up/email │ │
│ │ + guestId ──────► │
│ │ │ │
│ │ │ ── Insert user ───►│
│ │ │ ◄── User created ──│
│ │ │ │
│ │ │ ── Link guest │
│ │ │ assessment data │
│ │ │ ke new user ────►│
│ │ │ ◄── Linked ─────────│
│ │ │ │
│ │ ◄── Session + │ │
│ │ User data ─────│ │
│ ◄── Redirect ke │ │ │
│ onboarding ─────│ │ │

Module 2: Assessment Module​

Tanggung jawab: Menjalankan assessment (Ice Breaker, Universal, Adaptive), scoring, dan generate Learner Profile.

┌──────────────────────────────────────────────────────────┐
│ ASSESSMENT MODULE │
│ │
│ Service (assessment.service.ts) │
│ ├── startIceBreaker(userId?) │
│ │ → Buat session, return pertanyaan pertama │
│ ├── submitIceBreakerAnswer(sessionId, audioBlob) │
│ │ → STT → AI evaluate → return next Q or result │
│ ├── startUniversalAssessment(userId?) │
│ │ → Buat session, generate adaptive questions │
│ ├── submitUniversalAnswer(sessionId, audioBlob) │
│ │ → STT → AI evaluate → adjust difficulty → next Q │
│ ├── startAdaptiveConfirmation(userId) │
│ │ → Buat confirmation session berdasarkan profil awal │
│ ├── submitAdaptiveAnswer(sessionId, audioBlob) │
│ │ → STT → AI evaluate → finalize CEFR level │
│ ├── getAssessmentResult(sessionId) │
│ │ → Return skor lengkap + breakdown │
│ └── generateLearnerProfile(userId) │
│ → Aggregate semua skor → simpan Learner Profile │
│ │
│ AI Prompts (prompts/assessment.ts) │
│ ├── ICE_BREAKER_SYSTEM_PROMPT │
│ ├── UNIVERSAL_ASSESS_SYSTEM_PROMPT │
│ ├── ADAPTIVE_CONFIRM_SYSTEM_PROMPT │
│ ├── EVALUATE_SPEAKING_PROMPT │
│ └── GENERATE_LEARNER_PROFILE_PROMPT │
└──────────────────────────────────────────────────────────┘

Scoring Pipeline:

Audio Input
│
▼
┌──────────────┐
│ Deepgram │ ── STT ──► Raw Transcript
│ Nova-2 │ + Word timestamps
└──────────────┘ + Confidence scores
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│Pronunciation│ │ Grammar │ │ Fluency │
│ Analysis │ │ Check │ │ Metrics │
│ │ │ │ │ │
│• Phoneme │ │• LLM eval │ │• WPM │
│ accuracy │ │• Error │ │• Pause │
│• Stress │ │ classify │ │ pattern │
│ pattern │ │• Suggest │ │• Filler │
│• Intonation│ │ correct │ │ words │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└───────────────┼───────────────┘
│
▼
┌──────────────────┐
│ Scoring Engine │
│ │
│ pronunciation: 72│
│ grammar: 65 │
│ fluency: 58 │
│ vocabulary: 70 │
│ coherence: 62 │
│ confidence: 55 │
│ engagement: 80 │
│ │
│ CEFR: B1- │
└──────────────────┘

Module 3: Learning Module​

Tanggung jawab: Learning Blueprint, Learning Objectives, Missions, Progress Tracking.

┌──────────────────────────────────────────────────────────┐
│ LEARNING MODULE │
│ │
│ Service (learning.service.ts) │
│ ├── generateBlueprint(userId) │
│ │ → AI generates personalized learning plan │
│ │ → Buat LearningObjectives berdasarkan profil │
│ │ → Return blueprint + objectives │
│ │ │
│ ├── getDailyMissions(userId) │
│ │ → Pilih 3-5 missions berdasarkan: │
│ │ • Current learning objectives │
│ │ • User CEFR level │
│ │ • Weak areas dari learner profile │
│ │ • Variasi mode latihan │
│ │ → Return list of missions │
│ │ │
│ ├── startMission(userId, missionId) │
│ │ → Load mission config (mode, topic, difficulty) │
│ │ → Create practice session │
│ │ → Return session dengan stimulus/content │
│ │ │
│ ├── completeMission(userId, missionId, result) │
│ │ → Update mission status │
│ │ → Calculate XP gained │
│ │ → Check level up │
│ │ → Update streak │
│ │ → Queue: update-progress job │
│ │ │
│ ├── getProgress(userId) │
│ │ → Return: level, XP, streak, objectives progress, │
│ │ skill radar data, recent sessions │
│ │ │
│ └── getLearningPath(userId) │
│ → Return visual learning path map data │
└──────────────────────────────────────────────────────────┘

Mission Selection Algorithm:

// Pseudocode: Daily Mission Generation
async function generateDailyMissions(userId: string) {
const profile = await getLearnerProfile(userId);
const objectives = await getActiveObjectives(userId);
const recentModes = await getRecentPracticeModes(userId, 7); // last 7 days

const missions = [];

// 1. Priority mission: weakest skill area
const weakestSkill = findWeakestSkill(profile.skillRadar);
missions.push(createMission({
objective: objectives.find(o => o.targetSkill === weakestSkill),
mode: selectBestMode(weakestSkill, profile.cefrLevel),
difficulty: profile.cefrLevel,
priority: 'high',
}));

// 2. Continuation mission: current objective progress
const currentObjective = objectives.find(o => o.status === 'in_progress');
if (currentObjective) {
missions.push(createMission({
objective: currentObjective,
mode: selectVariedMode(currentObjective, recentModes),
difficulty: profile.cefrLevel,
priority: 'medium',
}));
}

// 3. Fun/variety mission: different mode for engagement
missions.push(createMission({
objective: pickRandom(objectives),
mode: pickFunMode(recentModes),
difficulty: adjustDifficulty(profile.cefrLevel, -1), // sedikit lebih mudah
priority: 'low',
}));

return missions;
}

Module 4: Conversation Module​

Tanggung jawab: Voice-based speaking practice, AI chat, real-time streaming, evaluation.

┌──────────────────────────────────────────────────────────┐
│ CONVERSATION MODULE │
│ │
│ Service (conversation.service.ts) │
│ ├── createSession(userId, config) │
│ │ → Create conversation session │
│ │ → Load relevant context: │
│ │ • Learner profile (CEFR, weak areas) │
│ │ • Mission config (mode, topic, stimulus) │
│ │ • AI companion personality │
│ │ → Cache context di Redis │
│ │ → Return session + opening message │
│ │ │
│ ├── processUserTurn(sessionId, audioBlob) │
│ │ → STT: audio → transcript │
│ │ → Get conversation context from Redis │
│ │ → Stream AI response (SSE): │
│ │ • event: token → text chunk │
│ │ • event: audio → TTS audio chunk │
│ │ • event: feedback → inline correction │
│ │ • event: done → turn complete │
│ │ → Save message pair to DB │
│ │ → Update context cache di Redis │
│ │ │
│ ├── endSession(sessionId) │
│ │ → Generate final evaluation │
│ │ → Calculate session scores │
│ │ → Queue: evaluate-speech job (detailed analysis) │
│ │ → Return summary + feedback │
│ │ │
│ └── getSessionHistory(userId, options) │
│ → Return paginated conversation history │
└──────────────────────────────────────────────────────────┘

SSE Streaming Implementation:

// Simplified SSE streaming flow
app.post('/api/v1/conversations/:sessionId/turn', async (c) => {
const sessionId = c.req.param('sessionId');
const audioBlob = await c.req.blob();

return streamSSE(c, async (stream) => {
// 1. Speech-to-Text
const transcript = await stt.transcribe(audioBlob);
await stream.writeSSE({
event: 'transcript',
data: JSON.stringify({ text: transcript.text }),
});

// 2. Get conversation context
const context = await redis.get(`ctx:${sessionId}`);
const messages = JSON.parse(context);
messages.push({ role: 'user', content: transcript.text });

// 3. Stream AI response
const result = streamText({
model: openai('gpt-4o-mini'),
system: buildSystemPrompt(session),
messages: messages,
});

let fullResponse = '';
for await (const chunk of result.textStream) {
fullResponse += chunk;
await stream.writeSSE({
event: 'token',
data: JSON.stringify({ text: chunk }),
});
}

// 4. Generate TTS for AI response
const audioBuffer = await tts.synthesize(fullResponse);
const audioUrl = await storage.upload(audioBuffer);
await stream.writeSSE({
event: 'audio',
data: JSON.stringify({ url: audioUrl }),
});

// 5. Save & done
await saveMessages(sessionId, transcript.text, fullResponse);
await stream.writeSSE({ event: 'done', data: '{}' });
});
});

Module 5: Content Module​

Tanggung jawab: CRUD domain, topic, speaking content, CEFR eligibility, media management.

┌──────────────────────────────────────────────────────────┐
│ CONTENT MODULE │
│ │
│ Hierarki Konten: │
│ Domain → Topic → Speaking Content │
│ │
│ Contoh: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Domain: "Business English" │ │
│ │ ├── Topic: "Meeting & Discussion" │ │
│ │ │ ├── SC: "Weekly Team Standup" (A2-B1) │ │
│ │ │ ├── SC: "Client Presentation" (B1-B2) │ │
│ │ │ └── SC: "Board Meeting" (B2-C1) │ │
│ │ ├── Topic: "Email & Writing" │ │
│ │ │ ├── SC: "Follow-up Email" (A2-B1) │ │
│ │ │ └── SC: "Proposal Writing" (B1-B2) │ │
│ │ └── Topic: "Negotiation" │ │
│ │ ├── SC: "Price Negotiation" (B1-B2) │ │
│ │ └── SC: "Contract Terms" (B2-C1) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ Service (content.service.ts) │
│ ├── listDomains() │
│ ├── getTopicsByDomain(domainId, cefrFilter?) │
│ ├── getContent(contentId) │
│ ├── getEligibleContent(userId) │
│ │ → Filter konten berdasarkan CEFR level user │
│ ├── createContent(data) [Admin] │
│ ├── updateContent(id, data) [Admin] │
│ └── uploadMedia(file) [Admin] │
└──────────────────────────────────────────────────────────┘

Module 6: Membership Module​

Tanggung jawab: Subscription management, payment processing, billing.

┌──────────────────────────────────────────────────────────┐
│ MEMBERSHIP MODULE │
│ │
│ Subscription Plans: │
│ ┌────────────────────────────────────────────────┐ │
│ │ FREE │ MONTHLY │ YEARLY │ │
│ │ │ │ │ │
│ │ • 1 Ice │ • Unlimited │ • Unlimited │ │
│ │ Breaker │ assessment │ assessment │ │
│ │ • Preview │ • Unlimited │ • Unlimited │ │
│ │ assessment │ practice │ practice │ │
│ │ • No missions│ • Blueprint │ • Blueprint │ │
│ │ │ • Missions │ • Missions │ │
│ │ Rp 0 │ Rp 99.000/mo│ Rp 799.000/yr │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Service (membership.service.ts) │
│ ├── getPlans() │
│ ├── subscribe(userId, planId) │
│ │ → Create order → redirect ke payment gateway │
│ ├── handlePaymentWebhook(payload) │
│ │ → Verify signature → activate subscription │
│ ├── getSubscription(userId) │
│ ├── cancelSubscription(userId) │
│ ├── checkAccess(userId, feature) │
│ │ → Return boolean (apakah user boleh akses fitur ini) │
│ └── getInvoices(userId) │
│ │
│ Payment Gateway: Midtrans (Indonesia) │
│ ├── Snap API untuk checkout │
│ ├── Webhook untuk notifikasi pembayaran │
│ └── Support: GoPay, OVO, BCA VA, Mandiri VA, CC │
└──────────────────────────────────────────────────────────┘

Error Handling Strategy​

// Custom Error Classes
class AppError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: unknown,
) {
super(message);
}
}

class NotFoundError extends AppError {
constructor(resource: string) {
super(404, 'NOT_FOUND', `${resource} tidak ditemukan`);
}
}

class UnauthorizedError extends AppError {
constructor(message = 'Silahkan login terlebih dahulu') {
super(401, 'UNAUTHORIZED', message);
}
}

class ForbiddenError extends AppError {
constructor(message = 'Anda tidak memiliki akses') {
super(403, 'FORBIDDEN', message);
}
}

class ValidationError extends AppError {
constructor(details: ZodError) {
super(400, 'VALIDATION_ERROR', 'Data tidak valid', details.flatten());
}
}

class QuotaExceededError extends AppError {
constructor(feature: string) {
super(429, 'QUOTA_EXCEEDED', `Kuota ${feature} habis. Upgrade ke Member!`);
}
}

// Global Error Handler Middleware
app.onError((err, c) => {
if (err instanceof AppError) {
return c.json({
success: false,
error: {
code: err.code,
message: err.message,
details: err.details,
},
}, err.statusCode);
}

// Unexpected error
logger.error(err);
Sentry.captureException(err);

return c.json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Terjadi kesalahan server',
},
}, 500);
});

Middleware Stack​

Request melewati middleware secara berurutan:

Request masuk
│
▼
┌──────────────┐
│ 1. Logger │ → Log method, path, status, duration
└──────┬───────┘
▼
┌──────────────┐
│ 2. CORS │ → Allow frontend origin
└──────┬───────┘
▼
┌──────────────┐
│ 3. Rate │ → Check Redis counter
│ Limit │ → 429 kalau melebihi limit
└──────┬───────┘
▼
┌──────────────┐
│ 4. Auth │ → Verify session token
│ (optional) │ → Inject user ke context
└──────┬───────┘
▼
┌──────────────┐
│ 5. Validate │ → Zod validation body/params/query
└──────┬───────┘
▼
┌──────────────┐
│ 6. Route │ → Execute business logic
│ Handler │
└──────┬───────┘
▼
┌──────────────┐
│ 7. Error │ → Catch & format errors
│ Handler │
└──────────────┘
│
▼
Response keluar