Badge System Evolution: Event-Driven Architecture (Part 2)
Badge System Evolution (Part 2)
Event-Driven Badge Evaluation
In Part 1, we built a badge system that could track progress and award achievements. It workedbut it came with a big design flaw:
Every feature had to manually update badges.
That’s not just annoying. It’s risky.
The Problem with Manual Badge Updates
Right now, badge logic is scattered all over the codebase.
app.post('/submit-quiz', (req, res) => {
// Quiz logic...
updateProgress(userId, "quiz_master", 1);
updateProgress(userId, "accuracy_streak", 1);
});
This creates several problems:
- ❌ You must remember to update badges everywhere
- ❌ New features can accidentally skip badge logic
- ❌ Adding new badges means editing existing business code
- ❌ Badge logic and feature logic are tightly coupled
In short: the system depends on humans remembering thingsand that never ends well.
The Core Idea: Let Events Drive Badges
Instead of telling the badge system what to do, we tell it what happened.
“A quiz was completed.” “A user logged in.” “A referral signup occurred.”
The badge system listens to those events and decides what (if anything) should happen.
Imperative vs Event-Driven
Before (Imperative):
submitQuiz();
updateBadgeA();
updateBadgeB();
updateBadgeC();
After (Event-Driven):
submitQuiz();
emitEvent("quiz_completed");
That’s it. Everything else happens automatically.
Event-Driven Badge Flow
The feature code only emits events. The badge system handles the rest.
Why This Architecture Is Better
-
Loose coupling Feature code knows nothing about badges.
-
Centralized logic All badge evaluation lives in one place.
-
Easy to extend Add new badges without touching existing features.
-
Hard to forget Events fire automaticallybadge evaluation always happens.
Emitting Events
Whenever something meaningful happens, emit an event:
events.emit("quiz_completed", {
userId,
quizId,
score: 85,
totalQuestions: 10
});
That’s the only responsibility of the feature.
The Badge Evaluator
The badge system listens for events and evaluates relevant badges.
class BadgeEvaluator {
constructor(db) {
this.db = db;
this.listeners = {};
}
register(eventType, badgeId, evaluator) {
if (!this.listeners[eventType]) {
this.listeners[eventType] = [];
}
this.listeners[eventType].push({ badgeId, evaluator });
}
async evaluateEvent(eventType, userId, data) {
const evaluators = this.listeners[eventType] || [];
for (const { badgeId, evaluator } of evaluators) {
try {
const result = await evaluator(userId, data);
if (result.shouldAward) {
await this.awardBadge(userId, badgeId, result.metadata);
} else if (result.progress !== undefined) {
await this.updateProgress(userId, badgeId, result.progress);
}
} catch (err) {
console.error(`Badge failed: ${badgeId}`, err);
}
}
}
}
Each badge gets its own evaluatorsmall, focused, testable.
Badge Evaluators (Examples)
First Quiz Badge
function firstQuizEvaluator(userId, eventData) {
const completedCount = db.query(
"SELECT COUNT(*) FROM user_quizzes WHERE user_id = ?",
[userId]
);
return {
shouldAward: completedCount === 1,
metadata: { quizId: eventData.quizId }
};
}
Progress Badge (Quiz Master – 10 quizzes)
function quizMasterEvaluator(userId) {
const progress = getProgress(userId, "quiz_master") + 1;
return {
shouldAward: progress >= 10,
progress: Math.min(progress, 10)
};
}
Threshold Badge (Accuracy Expert)
function accuracyExpertEvaluator(userId, eventData) {
const accuracy =
(eventData.score / eventData.totalQuestions) * 100;
return {
shouldAward: accuracy >= 85,
metadata: { accuracy }
};
}
Each evaluator:
- Does one thing
- Knows nothing about routes or controllers
- Can be tested in isolation
Wiring Everything Together
const evaluator = new BadgeEvaluator(db);
evaluator.register("quiz_completed", "first_quiz", firstQuizEvaluator);
evaluator.register("quiz_completed", "quiz_master", quizMasterEvaluator);
evaluator.register("quiz_completed", "accuracy_expert", accuracyExpertEvaluator);
And in your route:
app.post("/submit-quiz", async (req, res) => {
// Quiz logic...
await evaluator.evaluateEvent("quiz_completed", userId, {
quizId,
score,
totalQuestions
});
res.json({ success: true });
});
No badge logic. No coupling. Clean and safe.
Advanced Patterns
Streak-Based Badges
function dailyWarriorEvaluator(userId) {
const streak = getLoginStreak(userId);
return {
shouldAward: streak >= 7,
progress: Math.min(streak, 7)
};
}
Conditional Badges (Weekend-Only)
function weekendWarriorEvaluator() {
const today = new Date().getDay();
const isWeekend = today === 0 || today === 6;
if (!isWeekend) return { shouldAward: false };
// Weekend logic...
}
Badges can decide when not to run.
Database Adjustments
To support event-driven evaluation:
ALTER TABLE badges
ADD COLUMN trigger_events JSON;
ALTER TABLE user_badges
ADD COLUMN evaluation_metadata JSON;
Example badge definition:
INSERT INTO badges (id, name, trigger_events, target)
VALUES ('quiz_master', 'Quiz Master', '["quiz_completed"]', 10);
Performance Considerations
Multiple badges per event can be expensive.
Optimizations:
- Cache user state per event
- Evaluate badges in parallel
- Batch DB writes
async evaluateEvent(eventType, userId, data) {
const userState = await this.loadUserState(userId);
const results = await Promise.all(
this.getBadgesForEvent(eventType).map(badge =>
badge.evaluate(userState, data)
)
);
await this.batchPersist(userId, results);
}
Testing Becomes Easier
Unit Test (Single Badge)
test("awards first quiz badge", async () => {
const result = await firstQuizEvaluator("user1", {
quizId: "q1"
});
expect(result.shouldAward).toBe(true);
});
Integration Test (Event Flow)
test("quiz completion triggers multiple badges", async () => {
await evaluator.evaluateEvent("quiz_completed", "user1", {
score: 9,
totalQuestions: 10
});
expect(awardBadge).toHaveBeenCalledWith("user1", "first_quiz");
});
Key Takeaways (Part 2)
- Emit events, don’t update badges manually
- Badge logic belongs in one place
- Evaluators should be small and isolated
- Events make systems safer and easier to extend
- This architecture scales with complexity
In Part 3, we’ll build on this foundation with:
- Badge prerequisites
- Badge dependencies
- Notifications and user feedback
Event-driven systems turn badges from fragile add-ons into powerful engagement tools.
This is Part 2 of a series on building scalable badge systems. Part 1 covers the foundation with basic and progress-tracking badges.