Designing a Scalable Game Achievement System with Observer Pattern and Redis
The article presents a complete, event‑driven architecture for a game achievement system that uses the Observer pattern and Redis to achieve high cohesion, low coupling, millisecond‑level performance, and easy extensibility for millions of users and thousands of achievement types.
Introduction
In modern games, achievement systems are crucial for player retention but often become tightly coupled "hot spots" where changes to combat, quests, or login logic require modifications to achievement code. The goal is to build a highly cohesive, loosely coupled, and easily extensible achievement system.
Core Challenges and Design Goals
Decoupling : Business modules should not need to know the achievement system exists.
Extensibility : Adding new achievement types must not require changes to core code.
Performance : Support high‑concurrency event handling with millisecond response times.
Configurability : Designers can configure achievement rules without code changes.
Overall Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Game Business Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Combat │ │ Quest │ │ Social │ │ Economy │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──────┬──────┴─────────────┘ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │
│ │ │ Event Bus │ (Observer) │
│ │ └────────┬────────┘ │
│ │ │
└──────────────────────────────┼─────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────────┐
│ Achievement Layer │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Achievement Listener │ │
│ └────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Achievement Rule Engine │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ │ Count │ │ Condition│ │ Composite│ │ Sequence│ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ └────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Achievement Data (Redis) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ │ Progress│ │ Status │ │ Rank │ │ Stats │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘Core Design Principles
"Achievements are not handed out by the system; they are triggered by you!"
Core ideas:
1. Business modules only "emit events" and do not care who listens.
2. The achievement system only "listens to events" and does not interfere with business logic.
3. Redis provides fast data storage for real‑time progress tracking.Key Implementations
1. Event Definition
// pkg/event/event.go
package event
import "time"
type EventType string
const (
// combat related
EventMonsterKill EventType = "monster_kill"
EventPlayerKill EventType = "player_kill"
EventDamageDealt EventType = "damage_dealt"
EventDamageReceived EventType = "damage_received"
EventDeath EventType = "death"
// quest related
EventQuestComplete EventType = "quest_complete"
EventQuestAccept EventType = "quest_accept"
EventQuestAbandon EventType = "quest_abandon"
// social related
EventFriendAdd EventType = "friend_add"
EventChatSend EventType = "chat_send"
EventGuildJoin EventType = "guild_join"
// economy related
EventItemGet EventType = "item_get"
EventItemUse EventType = "item_use"
EventGoldEarn EventType = "gold_earn"
EventGoldSpend EventType = "gold_spend"
// role related
EventLevelUp EventType = "level_up"
EventLogin EventType = "login"
EventLogout EventType = "logout"
EventPlayTime EventType = "play_time"
)
type Event struct {
ID string `json:"id"`
Type EventType `json:"type"`
PlayerID string `json:"player_id"`
Timestamp time.Time `json:"timestamp"`
Data map[string]interface{} `json:"data"`
}
func NewEvent(eventType EventType, playerID string, data map[string]interface{}) *Event {
return &Event{ID: generateEventID(), Type: eventType, PlayerID: playerID, Timestamp: time.Now(), Data: data}
}
func generateEventID() string { return fmt.Sprintf("evt_%d", time.Now().UnixNano()) }2. Observer Pattern
// pkg/observer/observer.go
package observer
import "game/pkg/event"
type Observer interface {
OnEvent(evt *event.Event)
SubscribedEvents() []event.EventType
}
type Subject interface {
Attach(observer Observer)
Detach(observer Observer)
Notify(evt *event.Event)
}
type EventBus struct { observers map[event.EventType][]Observer }
func NewEventBus() *EventBus { return &EventBus{observers: make(map[event.EventType][]Observer)} }
func (eb *EventBus) Attach(observer Observer) {
for _, et := range observer.SubscribedEvents() { eb.observers[et] = append(eb.observers[et], observer) }
}
func (eb *EventBus) Detach(observer Observer) {
for et, obs := range eb.observers {
for i, o := range obs {
if o == observer { eb.observers[et] = append(obs[:i], obs[i+1:]...); break }
}
}
}
func (eb *EventBus) Notify(evt *event.Event) {
for _, o := range eb.observers[evt.Type] { go o.OnEvent(evt) }
}
func (eb *EventBus) Publish(evt *event.Event) { eb.Notify(evt) }3. Achievement Listener
// pkg/achievement/listener.go
package achievement
import (
"game/pkg/event"
"game/pkg/observer"
"log"
)
type AchievementListener struct { service *AchievementService }
func NewAchievementListener(s *AchievementService) *AchievementListener { return &AchievementListener{service: s} }
func (l *AchievementListener) SubscribedEvents() []event.EventType {
return []event.EventType{event.EventMonsterKill, event.EventPlayerKill, event.EventQuestComplete, event.EventFriendAdd, event.EventItemGet, event.EventLevelUp, event.EventLogin, event.EventGoldEarn}
}
func (l *AchievementListener) OnEvent(evt *event.Event) {
defer func() { if r := recover(); r != nil { log.Printf("achievement event handling failed: %v, event: %+v", r, evt) } }()
achievements := l.service.GetActiveAchievements(evt.Type)
for _, ach := range achievements {
if l.service.IsAchievementCompleted(evt.PlayerID, ach.ID) { continue }
progress := l.service.UpdateAchievementProgress(evt.PlayerID, ach, evt)
if l.service.CheckAchievementCondition(ach, progress) { l.service.UnlockAchievement(evt.PlayerID, ach) }
}
}
var _ observer.Observer = (*AchievementListener)(nil)4. Core Service Logic
// pkg/achievement/service.go
package achievement
import (
"context"
"encoding/json"
"fmt"
"game/pkg/event"
"game/pkg/redis"
"time"
)
type Achievement struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Type event.EventType `json:"type"`
Condition AchievementCondition `json:"condition"`
Reward AchievementReward `json:"reward"`
Priority int `json:"priority"`
}
type AchievementCondition struct {
Type string `json:"type"`
Target int `json:"target"`
Field string `json:"field"`
Operator string `json:"operator"`
TimeLimit time.Duration `json:"time_limit"`
}
type AchievementReward struct {
Title string `json:"title"`
Exp int `json:"exp"`
Gold int `json:"gold"`
ItemID string `json:"item_id"`
ItemCount int `json:"item_count"`
}
type AchievementProgress struct {
Current int `json:"current"`
Target int `json:"target"`
Completed bool `json:"completed"`
UpdatedAt time.Time `json:"updated_at"`
}
type AchievementService struct {
redisClient *redis.Client
achievements map[string]*Achievement
}
func NewAchievementService(rc *redis.Client) *AchievementService {
s := &AchievementService{redisClient: rc, achievements: make(map[string]*Achievement)}
s.loadAchievements()
return s
}
func (s *AchievementService) loadAchievements() {
s.achievements = map[string]*Achievement{
"ach_kill_monster_100": {
ID: "ach_kill_monster_100", Name: "怪物猎人", Description: "累计击杀 100 只怪物", Type: event.EventMonsterKill,
Condition: AchievementCondition{Type: "count", Target: 100, Field: "count", Operator: ">="},
Reward: AchievementReward{Title: "怪物猎人", Exp: 1000, Gold: 500},
},
"ach_kill_monster_1000": {
ID: "ach_kill_monster_1000", Name: "传奇猎手", Description: "累计击杀 1000 只怪物", Type: event.EventMonsterKill,
Condition: AchievementCondition{Type: "count", Target: 1000, Field: "count", Operator: ">="},
Reward: AchievementReward{Title: "传奇猎手", Exp: 10000, Gold: 5000},
},
"ach_level_50": {
ID: "ach_level_50", Name: "登峰造极", Description: "角色达到 50 级", Type: event.EventLevelUp,
Condition: AchievementCondition{Type: "count", Target: 50, Field: "level", Operator: ">="},
Reward: AchievementReward{Title: "登峰造极", Exp: 5000, Gold: 2000},
},
"ach_login_7days": {
ID: "ach_login_7days", Name: "忠诚勇士", Description: "累计登录 7 天", Type: event.EventLogin,
Condition: AchievementCondition{Type: "count", Target: 7, Field: "days", Operator: ">="},
Reward: AchievementReward{Title: "忠诚勇士", Exp: 2000, Gold: 1000},
},
"ach_gold_10000": {
ID: "ach_gold_10000", Name: "富甲一方", Description: "累计获得 10000 金币", Type: event.EventGoldEarn,
Condition: AchievementCondition{Type: "accumulate", Target: 10000, Field: "amount", Operator: ">="},
Reward: AchievementReward{Title: "富甲一方", Exp: 3000, Gold: 500},
},
}
}
func (s *AchievementService) GetActiveAchievements(eventType event.EventType) []*Achievement {
var res []*Achievement
for _, a := range s.achievements {
if a.Type == eventType { res = append(res, a) }
}
return res
}
func (s *AchievementService) UpdateAchievementProgress(playerID string, ach *Achievement, evt *event.Event) *AchievementProgress {
ctx := context.Background()
key := s.getProgressKey(playerID, ach.ID)
data, err := s.redisClient.Get(ctx, key).Bytes()
var prog *AchievementProgress
if err != nil || data == nil {
prog = &AchievementProgress{Target: ach.Condition.Target}
} else { json.Unmarshal(data, &prog) }
switch ach.Condition.Type {
case "count":
prog.Current++
case "accumulate":
if v, ok := evt.Data[ach.Condition.Field]; ok {
switch val := v.(type) {
case int: prog.Current += val
case float64: prog.Current += int(val)
}
}
case "max":
if v, ok := evt.Data[ach.Condition.Field]; ok {
switch val := v.(type) {
case int:
if val > prog.Current { prog.Current = val }
case float64:
if int(val) > prog.Current { prog.Current = int(val) }
}
}
}
prog.UpdatedAt = time.Now()
switch ach.Condition.Operator {
case ">=": prog.Completed = prog.Current >= ach.Condition.Target
case ">": prog.Completed = prog.Current > ach.Condition.Target
case "==": prog.Completed = prog.Current == ach.Condition.Target
case "<=": prog.Completed = prog.Current <= ach.Condition.Target
}
b, _ := json.Marshal(prog)
s.redisClient.Set(ctx, key, b, 0)
return prog
}
func (s *AchievementService) CheckAchievementCondition(ach *Achievement, prog *AchievementProgress) bool { return prog.Completed }
func (s *AchievementService) UnlockAchievement(playerID string, ach *Achievement) {
ctx := context.Background()
unlockKey := s.getUnlockKey(playerID)
exists, _ := s.redisClient.SIsMember(ctx, unlockKey, ach.ID).Result()
if exists { return }
s.redisClient.SAdd(ctx, unlockKey, ach.ID)
s.redisClient.Set(ctx, s.getRecordKey(playerID, ach.ID), time.Now().Unix(), 0)
s.grantReward(playerID, ach.Reward)
s.publishUnlockEvent(playerID, ach)
log.Printf("玩家 %s 解锁成就:%s", playerID, ach.Name)
}
func (s *AchievementService) grantReward(playerID string, r AchievementReward) {
ctx := context.Background()
if r.Exp > 0 { s.redisClient.HIncrBy(ctx, s.getPlayerExpKey(playerID), "exp", int64(r.Exp)) }
if r.Gold > 0 { s.redisClient.HIncrBy(ctx, s.getPlayerGoldKey(playerID), "gold", int64(r.Gold)) }
if r.ItemID != "" && r.ItemCount > 0 {
s.redisClient.IncrBy(ctx, s.getPlayerItemKey(playerID, r.ItemID), int64(r.ItemCount))
}
}
func (s *AchievementService) IsAchievementCompleted(playerID, achievementID string) bool {
ctx := context.Background()
unlockKey := s.getUnlockKey(playerID)
exists, _ := s.redisClient.SIsMember(ctx, unlockKey, achievementID).Result()
return exists
}
func (s *AchievementService) getProgressKey(p, a string) string { return fmt.Sprintf("achievement:progress:%s:%s", p, a) }
func (s *AchievementService) getUnlockKey(p string) string { return fmt.Sprintf("achievement:unlocked:%s", p) }
func (s *AchievementService) getRecordKey(p, a string) string { return fmt.Sprintf("achievement:record:%s:%s", p, a) }
func (s *AchievementService) getPlayerExpKey(p string) string { return fmt.Sprintf("player:exp:%s", p) }
func (s *AchievementService) getPlayerGoldKey(p string) string { return fmt.Sprintf("player:gold:%s", p) }
func (s *AchievementService) getPlayerItemKey(p, i string) string { return fmt.Sprintf("player:item:%s:%s", p, i) }
func (s *AchievementService) publishUnlockEvent(playerID string, ach *Achievement) {
// Example: publish to Redis Pub/Sub for notifications, leaderboard updates, etc.
// event := map[string]interface{}{ "player_id": playerID, "achievement_id": ach.ID, "achievement_name": ach.Name, "timestamp": time.Now() }
// s.redisClient.Publish("achievement_unlock", event)
}5. Integration Examples
Combat System
// services/combat/service.go
package combat
import (
"game/pkg/event"
"game/pkg/observer"
)
type CombatService struct { eventBus observer.Subject }
func NewCombatService(bus observer.Subject) *CombatService { return &CombatService{eventBus: bus} }
func (s *CombatService) KillMonster(playerID, monsterID, monsterType string) {
// 1. combat logic (damage, loot, etc.)
// 2. publish event
evt := event.NewEvent(event.EventMonsterKill, playerID, map[string]interface{}{ "monster_id": monsterID, "monster_type": monsterType, "count": 1 })
s.eventBus.Publish(evt)
// 3. continue other logic (drop, exp, …)
}
func (s *CombatService) DealDamage(playerID, targetID string, damage int, isCritical bool) {
evt := event.NewEvent(event.EventDamageDealt, playerID, map[string]interface{}{ "target_id": targetID, "damage": damage, "is_critical": isCritical })
s.eventBus.Publish(evt)
}Quest System
// services/quest/service.go
package quest
import (
"game/pkg/event"
"game/pkg/observer"
)
type QuestService struct { eventBus observer.Subject }
func NewQuestService(bus observer.Subject) *QuestService { return &QuestService{eventBus: bus} }
func (s *QuestService) CompleteQuest(playerID, questID string) error {
// quest completion logic
evt := event.NewEvent(event.EventQuestComplete, playerID, map[string]interface{}{ "quest_id": questID })
s.eventBus.Publish(evt)
return nil
}Social System
// services/social/service.go
package social
import (
"game/pkg/event"
"game/pkg/observer"
)
type SocialService struct { eventBus observer.Subject }
func NewSocialService(bus observer.Subject) *SocialService { return &SocialService{eventBus: bus} }
func (s *SocialService) AddFriend(playerID, friendID string) error {
// add‑friend logic
evt := event.NewEvent(event.EventFriendAdd, playerID, map[string]interface{}{ "friend_id": friendID, "count": 1 })
s.eventBus.Publish(evt)
return nil
}Main Program Assembly
// cmd/server/main.go
package main
import (
"game/pkg/observer"
"game/pkg/achievement"
"game/pkg/redis"
"game/services/combat"
"game/services/quest"
"game/services/social"
"log"
)
func main() {
// 1. Initialize Redis
redisClient := redis.NewClient(redis.Config{ Addr: "localhost:6379", Password: "", DB: 0 })
// 2. Create event bus
eventBus := observer.NewEventBus()
// 3. Create achievement service
achievementService := achievement.NewAchievementService(redisClient)
// 4. Register achievement listener
achievementListener := achievement.NewAchievementListener(achievementService)
eventBus.Attach(achievementListener)
// 5. Create business services
combatService := combat.NewCombatService(eventBus)
questService := quest.NewQuestService(eventBus)
socialService := social.NewSocialService(eventBus)
// 6. Server start (omitted)
log.Println("游戏服务器启动,成就系统已就绪")
// Simulate test
testAchievementSystem(combatService, questService, socialService, "player_001")
}
func testAchievementSystem(combatService *combat.CombatService, questService *quest.QuestService, socialService *social.SocialService, playerID string) {
// Simulate killing 100 monsters
for i := 0; i < 100; i++ { combatService.KillMonster(playerID, "monster_001", "goblin") }
// Simulate completing a quest
questService.CompleteQuest(playerID, "quest_001")
// Simulate adding a friend
socialService.AddFriend(playerID, "player_002")
}Redis Data‑Structure Design
Key Design Convention
┌─────────────────────────────────────────────────────────────────┐
│ Redis Key 命名规范 │
├─────────────────────────────────────────────────────────────────┤
│ achievement:progress:{player_id}:{achievement_id} // progress │
│ achievement:unlocked:{player_id} (Set) // unlocked │
│ achievement:record:{player_id}:{achievement_id} // unlock time │
│ player:exp:{player_id} (Hash) // exp │
│ player:gold:{player_id} (Hash) // gold │
│ player:item:{player_id}:{item_id} (String) // item count │
│ achievement:rank:{achievement_id} (ZSet) // leaderboard │
│ achievement:stats:{date} (Hash) // global stats │
└─────────────────────────────────────────────────────────────────┘Data Structure Selection
// pkg/redis/design.go
package redis
/*
Redis 数据结构选型说明:
1. String – simple counters, timestamps (e.g., player:item:{player_id}:{item_id})
2. Hash – object attributes (player:exp:{player_id}, player:gold:{player_id})
3. Set – unique collections (achievement:unlocked:{player_id})
4. Sorted Set – leaderboards (achievement:rank:{achievement_id})
5. List – event queues or history (achievement:history:{player_id})
6. Pub/Sub – real‑time notifications (achievement_unlock channel)
*/Leaderboard Implementation
// pkg/achievement/leaderboard.go
package achievement
import (
"context"
"fmt"
)
type LeaderboardService struct { redisClient *redis.Client }
func NewLeaderboardService(rc *redis.Client) *LeaderboardService { return &LeaderboardService{redisClient: rc} }
func (s *LeaderboardService) UpdateRank(ctx context.Context, achievementID, playerID string, score int64) {
key := fmt.Sprintf("achievement:rank:%s", achievementID)
s.redisClient.ZAdd(ctx, key, redis.Z{Score: float64(score), Member: playerID})
}
func (s *LeaderboardService) GetRank(ctx context.Context, achievementID, playerID string) int64 {
key := fmt.Sprintf("achievement:rank:%s", achievementID)
rank, _ := s.redisClient.ZRevRank(ctx, key, playerID).Result()
return rank + 1 // Redis ranks start at 0
}
type RankPlayer struct {
Rank int `json:"rank"`
PlayerID string `json:"player_id"`
Score int64 `json:"score"`
}
func (s *LeaderboardService) GetTopPlayers(ctx context.Context, achievementID string, n int) []*RankPlayer {
key := fmt.Sprintf("achievement:rank:%s", achievementID)
results, _ := s.redisClient.ZRevRangeWithScores(ctx, key, 0, int64(n-1)).Result()
var players []*RankPlayer
for i, r := range results {
players = append(players, &RankPlayer{Rank: i + 1, PlayerID: r.Member.(string), Score: int64(r.Score)})
}
return players
}Advanced Features
Composite Achievements (AND/OR)
// pkg/achievement/composite.go
package achievement
type CompositeCondition struct {
Type string `json:"type"`
Conditions []AchievementCondition `json:"conditions"`
}
func CheckCompositeCondition(playerID string, comp CompositeCondition, svc *AchievementService) bool {
switch comp.Type {
case "AND":
for _, c := range comp.Conditions { if !checkSingleCondition(playerID, c, svc) { return false } }
return true
case "OR":
for _, c := range comp.Conditions { if checkSingleCondition(playerID, c, svc) { return true } }
return false
default:
return false
}
}
func checkSingleCondition(playerID string, c AchievementCondition, svc *AchievementService) bool { return true }
var legendaryAdventurer = &Achievement{ID: "ach_legendary_adventurer", Name: "传奇冒险家", Description: "击杀 1000 只怪物,完成 50 个任务,等级达到 50 级", Type: event.EventMonsterKill, Condition: AchievementCondition{Type: "composite", Composite: CompositeCondition{Type: "AND", Conditions: []AchievementCondition{{Type: "count", Target: 1000, Field: "count", Operator: ">="}, {Type: "count", Target: 50, Field: "count", Operator: ">="}, {Type: "max", Target: 50, Field: "level", Operator: ">="}}}}}Limited‑Time Achievements
// pkg/achievement/limited.go
package achievement
import "time"
type LimitedAchievement struct {
Achievement
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
func (s *AchievementService) CheckTimeLimit(playerID string, a *LimitedAchievement) bool {
now := time.Now()
if now.Before(a.StartTime) || now.After(a.EndTime) { return false }
firstLoginKey := fmt.Sprintf("player:first_login:%s", playerID)
firstLogin, err := s.redisClient.Get(context.Background(), firstLoginKey).Time()
if err != nil || firstLogin.Before(a.StartTime) { return false }
return true
}
var springFestivalAchievement = &LimitedAchievement{Achievement: Achievement{ID: "ach_spring_festival", Name: "春节庆典", Description: "春节期间登录游戏 7 天", Type: event.EventLogin, Condition: AchievementCondition{Type: "count", Target: 7, Field: "days", Operator: ">="}}, StartTime: time.Date(2026,1,28,0,0,0,0,time.Local), EndTime: time.Date(2026,2,11,23,59,59,0,time.Local)}Hidden Achievements
// pkg/achievement/hidden.go
package achievement
type HiddenAchievement struct {
Achievement
RevealCondition string `json:"reveal_condition"`
}
func (s *AchievementService) ShouldReveal(playerID string, a *HiddenAchievement) bool {
prog := s.GetAchievementProgress(playerID, a.ID)
return float64(prog.Current)/float64(prog.Target) >= 0.5
}
var mysteriousKiller = &HiddenAchievement{Achievement: Achievement{ID: "ach_mysterious_killer", Name: "???", Description: "???", Type: event.EventPlayerKill, Condition: AchievementCondition{Type: "count", Target: 100, Field: "count", Operator: ">="}}, RevealCondition: "progress >= 50%"}Achievement Chains (Prerequisite)
// pkg/achievement/chain.go
package achievement
type AchievementChain struct {
ID string `json:"id"`
Name string `json:"name"`
AchievementIDs []string `json:"achievement_ids"`
}
func (s *AchievementService) CanUnlock(playerID, achID string) bool {
chain := s.findAchievementChain(achID)
if chain == nil { return true }
idx := s.findAchievementIndex(chain, achID)
if idx <= 0 { return true }
prev := chain.AchievementIDs[idx-1]
return s.IsAchievementCompleted(playerID, prev)
}
func (s *AchievementService) findAchievementChain(id string) *AchievementChain { return nil }
func (s *AchievementService) findAchievementIndex(chain *AchievementChain, id string) int { for i, v := range chain.AchievementIDs { if v == id { return i } } return -1 }
var hunterChain = &AchievementChain{ID: "chain_hunter", Name: "猎人之路", AchievementIDs: []string{"ach_kill_monster_100", "ach_kill_monster_500", "ach_kill_monster_1000", "ach_kill_monster_5000"}}Performance Optimizations
Batch Updates (Redis Pipeline)
// pkg/achievement/batch.go
package achievement
import (
"context"
"github.com/go-redis/redis/v8"
)
func (s *AchievementService) BatchUpdate(ctx context.Context, playerID string, events []*event.Event) error {
pipe := s.redisClient.Pipeline()
for _, evt := range events {
achs := s.GetActiveAchievements(evt.Type)
for _, ach := range achs {
prog := s.UpdateAchievementProgress(playerID, ach, evt)
if prog.Completed && !s.IsAchievementCompleted(playerID, ach.ID) { s.UnlockAchievement(playerID, ach) }
}
}
_, err := pipe.Exec(ctx)
return err
}Asynchronous Processing
// pkg/achievement/async.go
package achievement
import (
"context"
"game/pkg/event"
"sync"
)
type AsyncAchievementService struct {
*AchievementService
eventQueue chan *event.Event
workerNum int
wg sync.WaitGroup
}
func NewAsyncAchievementService(base *AchievementService, workers, queueSize int) *AsyncAchievementService {
s := &AsyncAchievementService{AchievementService: base, eventQueue: make(chan *event.Event, queueSize), workerNum: workers}
s.startWorkers()
return s
}
func (s *AsyncAchievementService) startWorkers() {
for i := 0; i < s.workerNum; i++ { s.wg.Add(1); go s.worker() }
}
func (s *AsyncAchievementService) worker() {
defer s.wg.Done()
for evt := range s.eventQueue { s.processEvent(evt) }
}
func (s *AsyncAchievementService) processEvent(evt *event.Event) {
achs := s.GetActiveAchievements(evt.Type)
for _, ach := range achs {
if s.IsAchievementCompleted(evt.PlayerID, ach.ID) { continue }
prog := s.UpdateAchievementProgress(evt.PlayerID, ach, evt)
if prog.Completed { s.UnlockAchievement(evt.PlayerID, ach) }
}
}
func (s *AsyncAchievementService) Publish(evt *event.Event) {
select { case s.eventQueue <- evt: default: s.processEvent(evt) }
}
func (s *AsyncAchievementService) Shutdown() { close(s.eventQueue); s.wg.Wait() }Caching Layer
// pkg/achievement/cache.go
package achievement
import (
"context"
"time"
)
type CacheConfig struct {
AchievementTTL time.Duration
ProgressTTL time.Duration
MaxSize int
}
type CachedAchievementService struct {
*AchievementService
achievementCache map[string]*Achievement
progressCache map[string]*AchievementProgress
config CacheConfig
}
func (s *CachedAchievementService) GetAchievementWithCache(id string) *Achievement {
if a, ok := s.achievementCache[id]; ok { return a }
a := s.loadAchievementFromDB(id)
if a != nil { s.achievementCache[id] = a }
return a
}
func (s *CachedAchievementService) GetProgressWithCache(ctx context.Context, playerID, achID string) *AchievementProgress {
key := playerID + ":" + achID
if p, ok := s.progressCache[key]; ok { return p }
p := s.GetAchievementProgress(playerID, achID)
s.progressCache[key] = p
return p
}
func (s *CachedAchievementService) startCacheCleaner() {
ticker := time.NewTicker(time.Minute)
go func() { for range ticker.C { s.cleanExpiredCache() } }()
}
func (s *CachedAchievementService) cleanExpiredCache() { /* LRU or TTL logic */ }Monitoring & Alerting
Metrics (Prometheus)
// pkg/achievement/metrics.go
package achievement
import "github.com/prometheus/client_golang/prometheus"
var (
achievementUnlockTotal = prometheus.NewCounterVec(prometheus.CounterOpts{Name: "achievement_unlock_total", Help: "Total number of achievement unlocks"}, []string{"achievement_id"})
achievementProgressUpdate = prometheus.NewCounterVec(prometheus.CounterOpts{Name: "achievement_progress_update_total", Help: "Total number of progress updates"}, []string{"achievement_id"})
achievementProcessDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "achievement_process_duration_seconds", Help: "Processing latency", Buckets: prometheus.DefBuckets}, []string{"event_type"})
achievementQueueSize = prometheus.NewGauge(prometheus.GaugeOpts{Name: "achievement_queue_size", Help: "Current size of the async queue"})
)
func init() {
prometheus.MustRegister(achievementUnlockTotal, achievementProgressUpdate, achievementProcessDuration, achievementQueueSize)
}Structured Logging
// pkg/achievement/logger.go
package achievement
import "go.uber.org/zap"
type AchievementLogger struct { logger *zap.Logger }
func (l *AchievementLogger) LogUnlock(playerID string, a *Achievement) {
l.logger.Info("achievement_unlocked", zap.String("player_id", playerID), zap.String("achievement_id", a.ID), zap.String("achievement_name", a.Name))
}
func (l *AchievementLogger) LogProgress(playerID string, a *Achievement, cur, target int) {
l.logger.Debug("achievement_progress", zap.String("player_id", playerID), zap.String("achievement_id", a.ID), zap.Int("progress", cur), zap.Int("target", target))
}
func (l *AchievementLogger) LogError(playerID string, err error) {
l.logger.Error("achievement_error", zap.String("player_id", playerID), zap.Error(err))
}Conclusion
The observer‑based, event‑driven architecture combined with Redis delivers a highly decoupled, scalable, and maintainable achievement system suitable for large‑scale games, mobile titles, and any platform that requires real‑time player progression tracking.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
