kan01234 - Software Engineer Notes

Logo

A backend engineer's journey of learning and growth.

View the Project on GitHub kan01234/post

24 June 2025

System Design: Building a Fair and Scalable Gacha System for Mobile Games

by kan01234

🧱 Objective

Design a Gacha (ガチャ) System for a mobile game that:

✅ Functional Requirements

❌ Non-Functional Requirements

🧩 High-Level Components

+------------------+
|    Game Client   |
+--------+---------+
         |
         v
+--------------------+          +-----------------------+
|     API Gateway    |--------->|    Gacha Service      |
+--------------------+          +----------+------------+
                                           |
                         +-----------------+-------------------+
                         |                                     |
              +----------v-----------+             +-----------v----------+
              |   RNG & Drop Engine  |             |  Player Inventory DB  |
              +----------+-----------+             +-----------+----------+
                         |                                     |
           +-------------v------------+         +--------------v-------------+
           |  Gacha Config Service    |         |   Transaction / Currency    |
           |  (rates, events, pity)   |         |   Service (Wallets)         |
           +--------------------------+         +-----------------------------+

🎲 What is RNG?

RNG stands for Random Number Generator.

In games (especially gacha games), it’s the core of randomness — it’s how the game decides what you get when you “pull” (e.g., which item or character).

Example:

It’s like rolling a 100-sided die behind the scenes.

🧷 What is Pity?

Pity is a player-friendly mechanic that increases your chance of getting a rare item after multiple unlucky tries.

There are two main types:

Type Description
🧠 Soft Pity Your chance increases slightly each time you don’t get the rare item. Example: UR starts at 1.5%, goes up by 0.1% per pull after 70 pulls.
💎 Hard Pity A guaranteed rare drop if you haven’t gotten it after a fixed number of pulls (e.g., UR guaranteed at pull #90).

Example:

Pull Count UR Chance
1 1.5%
70 1.5%
71 1.6%
80 2.5%
90 💥100% (guaranteed)**

This system is used in games like Genshin Impact, Fate/Grand Order, and others — it builds trust and reduces frustration for unlucky players.

🎲 Core Logic: RNG & Drop Engine

🧠 You may precompute drop tables per player or shard RNG seed per user/session for anti-cheating.

🗃️ Data Models

🎰 Gacha Config (in DB or Config Store)

Field Type Description
gacha_id UUID Unique Gacha pool
rarity_tiers JSON { SSR: 1.5%, SR: 12%, R: 86.5% }
items JSON List of items by rarity
pity_limit int Guaranteed SSR after N pulls
rate_up_items List Limited-time boosted items

🧾 Roll History

Field Type
player_id UUID
gacha_id UUID
item_id UUID
rarity String
timestamp DateTime
is_rate_up Boolean
used_pity Boolean

⚙️ Flow: Single Roll

Client sends POST /roll with gachaId and currency info

Gacha Service:

  1. Validates request & user session
  2. Checks currency via Transaction Service
  3. Gets config for that gacha
  4. Applies RNG + pity logic
  5. Returns item
  6. Writes roll result to History DB
  7. Updates Player Inventory
  8. Response: { item_id, rarity, used_pity }

⚡ Performance & Scale

Component Strategy
Gacha Logic Stateless, horizontally scalable API
Config Store Cached in memory, or in CDN / Redis
Roll History Write-heavy, use append-only logs
Inventory Strong consistency, ideally in RDB
RNG Use secure RNG, consider DRBG
Wallet Pessimistic lock or atomic update

🔐 Fairness & Anti-Cheat

🔭 Observability & Analytics

Metric Use
Rolls per second Scale monitoring
SSR/SR drop rates Fairness and bug detection
Pity counter triggered Track behavior and user experience
Currency consumption Revenue analytics
Unique items rolled Event success metrics

🔁 Retry & Idempotency

🧱 Datastore Design

Data Store Why
Gacha Configs Redis / S3 Fast load, infrequent writes
Roll History Append-only DB or Kafka Easy replay, audit trail
Inventory / Player PostgreSQL ACID for inventory consistency
Wallet PostgreSQL Atomic balance updates
Analytics Kafka + Druid Real-time insights

🎯 Fairness Problem

If each player rolls independently using RNG, then in theory, over enough rolls, the actual distribution should match the configured rates (e.g., 1% UR, 10% SR, etc.). But in practice:

💡 Solution Options

✅ 1. Soft RNG + Pity System (Industry Standard)

Most games (e.g., Genshin, FGO, etc.):

This simplifies scaling, ensures fairness per user, and is acceptable to players if the mechanics are transparent.

🎯 This is the most common and scalable way to handle gacha drop fairness.


❗️2. Global Rate Enforcement (Not Recommended for Realtime Gacha)

If you want to enforce a global UR quota, you’d need to:

This is problematic:

Only some limited-time lottery events (not real-time gacha) might use this.


🧪 3. Pre-shuffled “Drop Bags” (Deterministic Fairness)

Used in competitive card games or small-scale events.

Mechanism:

Pros:

Cons:


🧠 Better Compromise: Per-Player Bounded RNG

Use a per-player strategy like:

This doesn’t enforce global fairness, but provides a bounded player experience, so no one is “eternally unlucky”.

🔐 Summary: How to Ensure Fairness & Player Trust

Technique Global Rate Control Player Experience Scalable Used In Practice
Per-player RNG + Pity ✅ Excellent ✅ Yes ✅ Widely used
Global Drop Rate Enforcement ❌ Bad ❌ No ❌ Rare
Pre-shuffled Drop Bags ✅ (per batch) ✅ Fair 🚫 No 🔶 Event use only
Per-player Bounded RNG ✅ Controlled ✅ Yes ✅ Often combined

🏁 TL;DR — Key Design Choices

Component Choice Justification
RNG Secure RNG + server-only Prevents manipulation
Gacha config Cached + dynamic Event support, fast load
Currency Wallet service (locked) Consistent balance and auditability
Inventory RDB Reliable item tracking
History Append log For audit, replays, user support
Observability Metrics + logs + alerts Operations visibility
tags: system-design