To demonstrate how to apply Domain-Driven Design (DDD) to a word memorization software using your tech stack (Next.js and TypeScript), I’ll create a simplified example that focuses on the core domain concepts, tactical patterns, and their implementation. The example will include a domain model for managing a user’s word-learning progress, structured around DDD principles like entities, aggregates, repositories, and bounded contexts. I’ll provide a code example that integrates with Next.js and TypeScript, keeping the implementation practical and focused on the domain.
Overview of the Word Memorization Software
The software allows users to learn and memorize vocabulary by studying words, tracking progress, and reviewing words based on a spaced repetition algorithm (e.g., reviewing words at increasing intervals to aid retention). The domain revolves around managing words, user progress, and study sessions.
Domain Analysis
- Domain: Vocabulary learning.
- Bounded Contexts:
- Vocabulary Management: Manages the creation and storage of words and their metadata (e.g., definitions, examples).
- Learning Progress: Tracks a user’s progress for each word, including mastery level and review schedules.
- Study Session: Manages active study sessions where users review words and answer questions.
- Ubiquitous Language:
- Key terms:
Word,User,Progress,StudySession,MasteryLevel,ReviewSchedule.
- Key terms:
- Focus for the Example: We’ll focus on the Learning Progress bounded context, as it’s central to tracking a user’s word memorization journey.
Domain Model
- Entity:
WordProgress(tracks a user’s progress for a specific word, identified by a unique combination of user ID and word ID). - Value Object:
MasteryLevel(represents the user’s proficiency with a word, e.g., “Beginner,” “Intermediate,” “Mastered”). - Aggregate:
WordProgressis the aggregate root, encapsulating the user’s progress data and enforcing consistency (e.g., updating mastery level based on study results). - Repository:
WordProgressRepository(handles persistence ofWordProgressaggregates). - Domain Event:
WordProgressUpdated(published when a user’s progress changes, e.g., after a study session). - Domain Service:
SpacedRepetitionService(calculates the next review date based on the mastery level and last review).
Implementation Plan
- Define the domain model (entities, value objects, aggregates) in TypeScript.
- Create a repository interface for persistence.
- Implement a domain service for spaced repetition logic.
- Integrate the model with a Next.js API route to handle user interactions (e.g., updating progress after a study session).
- Use a simple in-memory repository for the example (in a real app, you’d use a database like PostgreSQL or MongoDB).
Below is the code example, structured according to DDD principles and tailored to your Next.js + TypeScript stack.
// Domain: Learning Progress Bounded Context
// Ubiquitous Language: WordProgress, MasteryLevel, ReviewSchedule, SpacedRepetitionService
import { v4 as uuidv4 } from 'uuid';
// Value Object: MasteryLevel
class MasteryLevel {
private readonly value: string;
constructor(value: string) {
const validLevels = ['Beginner', 'Intermediate', 'Mastered'];
if (!validLevels.includes(value)) {
throw new Error(`Invalid mastery level: ${value}`);
}
this.value = value;
}
getValue(): string {
return this.value;
}
isMastered(): boolean {
return this.value === 'Mastered';
}
nextLevel(): MasteryLevel {
if (this.value === 'Beginner') return new MasteryLevel('Intermediate');
if (this.value === 'Intermediate') return new MasteryLevel('Mastered');
return this; // Already Mastered
}
}
// Value Object: ReviewSchedule
class ReviewSchedule {
constructor(public readonly nextReviewDate: Date) {}
isDue(currentDate: Date): boolean {
return currentDate >= this.nextReviewDate;
}
}
// Entity: WordProgress (Aggregate Root)
class WordProgress {
private readonly id: string; // Unique ID for the progress (e.g., userId_wordId)
private readonly userId: string;
private readonly wordId: string;
private masteryLevel: MasteryLevel;
private reviewSchedule: ReviewSchedule;
constructor(userId: string, wordId: string, masteryLevel: MasteryLevel, reviewSchedule: ReviewSchedule) {
this.id = `${userId}_${wordId}`;
this.userId = userId;
this.wordId = wordId;
this.masteryLevel = masteryLevel;
this.reviewSchedule = reviewSchedule;
}
getId(): string {
return this.id;
}
getMasteryLevel(): MasteryLevel {
return this.masteryLevel;
}
getReviewSchedule(): ReviewSchedule {
return this.reviewSchedule;
}
// Business logic: Update progress based on study result
updateProgress(correct: boolean, spacedRepetitionService: SpacedRepetitionService): WordProgressUpdated {
if (correct && !this.masteryLevel.isMastered()) {
this.masteryLevel = this.masteryLevel.nextLevel();
}
this.reviewSchedule = spacedRepetitionService.calculateNextReview(this.masteryLevel);
return new WordProgressUpdated(this.id, this.userId, this.wordId, this.masteryLevel.getValue());
}
}
// Domain Event: WordProgressUpdated
class WordProgressUpdated {
constructor(
public readonly progressId: string,
public readonly userId: string,
public readonly wordId: string,
public readonly newMasteryLevel: string
) {}
}
// Domain Service: SpacedRepetitionService
class SpacedRepetitionService {
calculateNextReview(masteryLevel: MasteryLevel): ReviewSchedule {
const now = new Date();
let daysToAdd = 1;
switch (masteryLevel.getValue()) {
case 'Beginner':
daysToAdd = 1;
break;
case 'Intermediate':
daysToAdd = 3;
break;
case 'Mastered':
daysToAdd = 7;
break;
}
const nextReviewDate = new Date(now.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
return new ReviewSchedule(nextReviewDate);
}
}
// Repository Interface
interface WordProgressRepository {
findById(id: string): Promise<WordProgress | null>;
save(progress: WordProgress): Promise<void>;
}
// In-Memory Repository (for demonstration; replace with database in production)
class InMemoryWordProgressRepository implements WordProgressRepository {
private store: Map<string, WordProgress> = new Map();
async findById(id: string): Promise<WordProgress | null> {
return this.store.get(id) || null;
}
async save(progress: WordProgress): Promise<void> {
this.store.set(progress.getId(), progress);
}
}
// Factory for creating WordProgress
class WordProgressFactory {
static create(userId: string, wordId: string, spacedRepetitionService: SpacedRepetitionService): WordProgress {
const initialMastery = new MasteryLevel('Beginner');
const initialSchedule = spacedRepetitionService.calculateNextReview(initialMastery);
return new WordProgress(userId, wordId, initialMastery, initialSchedule);
}
}
// Example Usage in a Next.js API Route
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const { userId, wordId, correct } = req.body;
const spacedRepetitionService = new SpacedRepetitionService();
const repository = new InMemoryWordProgressRepository();
// Load or create WordProgress
let progress = await repository.findById(`${userId}_${wordId}`);
if (!progress) {
progress = WordProgressFactory.create(userId, wordId, spacedRepetitionService);
}
// Update progress
const event = progress.updateProgress(correct, spacedRepetitionService);
await repository.save(progress);
// Return response
res.status(200).json({
progressId: event.progressId,
userId: event.userId,
wordId: event.wordId,
masteryLevel: event.newMasteryLevel,
nextReviewDate: progress.getReviewSchedule().nextReviewDate,
});
} else {
res.status(405).json({ message: 'Method not allowed' });
}
}
Explanation of the Code
- Bounded Context: The code focuses on the Learning Progress context, managing how users progress in memorizing words.
- Entities and Value Objects:
WordProgressis the aggregate root, encapsulatingMasteryLevelandReviewSchedule.MasteryLevelis a value object that enforces valid proficiency levels.ReviewScheduleis a value object that tracks when a word should be reviewed next.
- Domain Service:
SpacedRepetitionServiceencapsulates the logic for calculating review intervals based on mastery level (e.g., 1 day for Beginner, 3 days for Intermediate). - Repository:
InMemoryWordProgressRepositoryprovides a simple persistence layer. In a real app, you’d replace it with a database-backed repository (e.g., using Prisma or MongoDB). - Domain Event:
WordProgressUpdatedis published when progress changes, allowing other contexts (e.g., Study Session) to react (e.g., update session statistics). - Factory:
WordProgressFactoryensures newWordProgressinstances are created with valid initial states. - Next.js Integration: The API route (
/api/word-progress) handles requests to update progress (e.g., after a user answers a word correctly or incorrectly in a study session).
How to Use in a Next.js App
- File Structure:
- Place the domain code in
lib/domain/word-memorization-domain.ts. - The API route can be placed in
pages/api/word-progress.ts.
- Place the domain code in
- Frontend:
- Create a React component to call the API (e.g., a study interface where users answer word questions).
- Use
fetchoraxiosto send POST requests to/api/word-progresswith{ userId, wordId, correct }.
- Persistence:
- Replace
InMemoryWordProgressRepositorywith a real database (e.g., PostgreSQL with Prisma) for production.
- Replace
- Event Handling:
- Use a message queue (e.g., Redis or RabbitMQ) to publish and handle
WordProgressUpdatedevents for cross-context communication.
- Use a message queue (e.g., Redis or RabbitMQ) to publish and handle
Extending the Example
- Vocabulary Management Context: Add a
Wordentity with attributes like definition, translation, and example sentences. Create aWordRepositoryto manage word data. - Study Session Context: Implement a
StudySessionaggregate to manage active study sessions, selecting words due for review based onReviewSchedule. - Context Mapping: Use domain events to integrate contexts. For example, the Learning Progress context can subscribe to
WordAddedevents from the Vocabulary Management context to initializeWordProgressfor new words.
This example demonstrates DDD’s core principles (ubiquitous language, aggregates, bounded contexts) in a practical TypeScript and Next.js implementation.
Comments