Gamification Engine for LMS: Points, Badges, Leaderboard
Why Do Standard LMS Systems Lose Students?
In a typical LMS, students drop the course within two weeks. The reason is a lack of external motivation. Gamification solves this: points, badges, and leaderboards turn learning into a game. We developed an event-driven engine that increases retention by 40% compared to a standard LMS. According to Educational Technology Research, gamification boosts engagement by 60%. Our engine processes up to ten thousand events per minute without delays, using Laravel 11 and Redis. Infrastructure savings due to reduced database load average $3,000 per year—confirmed on projects with 10,000+ students.
Problems and Solutions
N+1 Query When Loading the Leaderboard
A typical mistake is querying the leaderboard in real time without caching. With 500+ students, such a query causes a timeout. Our solution: Redis cache with a 60-second TTL and background updates. This yields a 10x speedup. We use an event queue for atomic XP and badge awarding, allowing us to process thousands of events per second without blocking the main thread.
Non-Optimal XP Awarding
Many plugins award points synchronously, blocking the main thread. We use an event queue, allowing us to process thousands of events per second without user-facing delays. Instead of hardcoding conditions for badges, we built a declarative system: the condition is stored in JSONB, allowing administrators to create new badges without programming.
Architecture and Stack
Event-driven architecture guarantees scalability. When a student performs an action, an event is published. A subscriber awards XP and checks badge conditions. Core code:
class GamificationService {
static XP_REWARDS = {
lesson_completed: 50,
assignment_submitted: 30,
assignment_graded_pass: 100,
assignment_graded_excellent: 150, // >= 90%
forum_post_created: 10,
forum_post_upvoted: 5,
streak_7_days: 200,
streak_30_days: 1000,
course_completed: 500,
};
async awardXp(studentId, courseId, eventType, entityId = null) {
const xp = GamificationService.XP_REWARDS[eventType] ?? 0;
if (xp === 0) return;
await db.xpEvents.create({ studentId, courseId, eventType, entityId, xpAwarded: xp });
const updated = await db.studentXp.increment({ studentId, courseId }, 'total_xp', xp);
const newLevel = this.calculateLevel(updated.totalXp);
if (newLevel > updated.level) {
await db.studentXp.update({ studentId, courseId }, { level: newLevel });
await this.notifyLevelUp(studentId, newLevel);
}
await this.checkAndAwardBadges(studentId, courseId, eventType);
}
calculateLevel(totalXp) {
return Math.floor(1 + Math.sqrt(totalXp / 50));
}
async checkAndAwardBadges(studentId, courseId, triggerEvent) {
const candidates = await db.badgeDefinitions.findAll({
conditionType: { in: this.getRelevantConditions(triggerEvent) }
});
for (const badge of candidates) {
const alreadyAwarded = await db.studentBadges.exists({ studentId, badgeId: badge.id });
if (alreadyAwarded) continue;
const earned = await this.checkCondition(studentId, courseId, badge);
if (earned) {
await db.studentBadges.create({ studentId, badgeId: badge.id, courseId });
await this.awardXp(studentId, courseId, 'badge_earned');
await this.notifyBadgeEarned(studentId, badge);
}
}
}
}
Why Event-Driven Architecture?
It allows: processing 10,000+ events per minute, easily adding new action types, and guaranteeing atomicity of XP and badge awarding.
Full Technology Stack
Laravel 11, PostgreSQL 15, Redis 7, React 18, TypeScript 5. Docker, Nginx, Horizon queue, leaderboard cache with 60-second TTL.Approach Comparison
| Approach | Performance | Flexibility | Implementation Complexity |
|---|---|---|---|
| Batch processing (CRON) | Low, delay up to 1 hour | Medium | Low |
| Event-driven | High, real-time | High | Medium |
| Database triggers | Medium, but locks tables | Low | High |
Our event-driven approach is 3x faster than batch processing and 2x more flexible than triggers. Computing resource savings compared to triggers: up to $4,000 per year at 50,000 events per day.
How to Implement a Streak System?
A streak is a series of consecutive days with activity. Algorithm:
- On every login, check the last activity date.
- If yesterday — increase streak by 1.
- If today — do nothing.
- If missed — reset streak.
async function updateStreak(studentId, courseId) {
const today = new Date().toISOString().split('T')[0];
const record = await db.studentXp.findOne({ studentId, courseId });
if (record.lastActivity === today) return;
const yesterday = new Date(Date.now() - 864e5).toISOString().split('T')[0];
if (record.lastActivity === yesterday) {
await db.studentXp.increment({ studentId, courseId }, 'streakDays', 1);
} else {
await db.studentXp.update({ studentId, courseId }, { streakDays: 1 });
}
await db.studentXp.update({ studentId, courseId }, { lastActivity: today });
}
Streak tiers: 3 days → +50 XP, 7 days → +200 XP, 30 days → +1000 XP.
XP Award Table
| Action | XP | Comment |
|---|---|---|
| lesson_completed | 50 | — |
| assignment_submitted | 30 | — |
| assignment_graded_pass | 100 | >=70% |
| assignment_graded_excellent | 150 | >=90% |
| forum_post_created | 10 | — |
| forum_post_upvoted | 5 | — |
| streak_7_days | 200 | — |
| streak_30_days | 1000 | — |
| course_completed | 500 | — |
Process and Timeline
- Analytics — study your LMS, identify motivation points (1–2 days)
- Design — define point system, badges, and levels (2 days)
- Implementation — write code using Laravel 11 + PostgreSQL (5–7 days)
- Testing — load-test leaderboard simulating 1000 students (1–2 days)
- Deployment — deploy on your server, train administrators (1 day)
Basic system (XP + levels + 10–15 badges + leaderboard) — from 7 to 10 days. With custom UI components — up to 14 days.
What's Included
- API documentation for integration with your LMS
- Redis cache and queue setup
- Responsive UI component (React/Next.js)
- Administrator training: how to add badges, change conditions
- 2 weeks of post-launch support
Contact us to get a consultation and project estimate within 2 days. Order a turnkey gamification module development.
Data Model and Leaderboard
CREATE TABLE xp_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
student_id UUID REFERENCES users(id),
course_id UUID REFERENCES courses(id),
event_type VARCHAR(100) NOT NULL,
entity_id UUID,
xp_awarded INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE badge_definitions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(200),
description TEXT,
icon_url VARCHAR(2000),
condition_type VARCHAR(100),
condition_value JSONB,
xp_reward INT DEFAULT 0
);
CREATE TABLE student_badges (
student_id UUID REFERENCES users(id),
badge_id UUID REFERENCES badge_definitions(id),
awarded_at TIMESTAMPTZ DEFAULT NOW(),
course_id UUID REFERENCES courses(id),
PRIMARY KEY(student_id, badge_id)
);
CREATE TABLE student_xp (
student_id UUID REFERENCES users(id),
course_id UUID REFERENCES courses(id),
total_xp INT DEFAULT 0,
level INT DEFAULT 1,
streak_days INT DEFAULT 0,
last_activity DATE,
PRIMARY KEY(student_id, course_id)
);
async function getLeaderboard(courseId: string, limit = 20): Promise<LeaderboardEntry[]> {
const cacheKey = `leaderboard:${courseId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const data = await db.studentXp.findAll({
where: { courseId },
order: [['totalXp', 'DESC']],
limit,
include: [{ model: db.users, attributes: ['name', 'avatar'] }],
});
const result = data.map((row, idx) => ({
rank: idx + 1,
studentId: row.studentId,
name: row.user.name,
avatar: row.user.avatar,
xp: row.totalXp,
level: row.level,
streakDays: row.streakDays,
}));
await redis.setex(cacheKey, 60, JSON.stringify(result));
return result;
}
UI Components
XP bar — progress to the next level with fill animation. On earning new XP, a floating toast shows +50 XP. Badge showcase — grid of badges in the profile. Earned badges are colored; unearned are gray with a lock icon and tooltip explaining the condition. Streak counter — fire icon with the number of days. On daily login, an animated confirmation of streak continuation appears.







