Gamification Platform HLD | Phase 1 (UPI + FS)

TL;DR

Phase 1 is an event-driven gamification platform that stays off the payment-critical path while supporting UPI and FS-triggered games, with external cohort eligibility resolved via a single LaunchDarkly flag and rewards dispatched asynchronously. Events are normalized into a canonical contract, conditionally coalesced across systems, and evaluated by a config-driven engine. App UX is served through low-latency Postgres-backed projections; Firestore is a best-effort post-transaction wakeup channel with no durability requirement. Side effects are separated into two purpose-specific outboxes — reward_outbox and side_effects_outbox — each with its own retry policy and criticality level.

[Diagram]

1. Purpose and context

The platform is an event-driven gamification backend initially focused on UPI and Financial Services (FS), while establishing a foundation that can evolve into a broader platform to handle new event sources & game archetypes without re-architecting the core. It must remain off the payment-critical path, so progression, rewards, notifications, and analytics publication happen asynchronously, while the user experience still feels near real time through projections and post-transaction client notifications.

2. Scope and non-goals

In scope for Phase 1

Deferred / out of scope

3. Architecture principles

  1. Off the critical path: payment and FS success flows must not depend on gamification runtime health.
  2. Event-driven write, projection-based read: state changes happen asynchronously; clients read precomputed projections.
  3. Canonical event contract: all source rails are normalized into a single contract.
  4. Config-driven engine: games are versioned JSON configs interpreted by a generic engine.
  5. External cohort routing, internal state authority: eligibility is resolved externally via the LD flag on every event; enrollment, progress, reward, and projections remain backend authoritative.
  6. LD flag as the sole enrollment gate: there is no separate enrollment trigger, batch seed, or refresh-driven enrollment. A user is enrolled inline when the LD flag returns a game_id and no enrollment record exists for that user and game instance.
  7. Idempotency at every stage: duplicate or replayed events must not create duplicate side effects.
  8. Outbox-first side effects: rewards, notifications, and analytics publish only from committed outbox records. Firestore delivery is best-effort and carries no durability guarantee; authoritative state is always available via Postgres-backed projections and BFF APIs.
  9. Purpose-specific outboxes: two outbox tables - reward_outbox and side_effects_outbox - each tuned for its own retry policy, retention, and criticality level.

4. High-level architecture

Component view

[Diagram]

Cohort definition, segment creation, and LaunchDarkly segment mapping are managed outside the gamification platform by business teams through the existing segment-as-a-service capability. Gamification consumes one externally managed static multivariate flag using customer hash as context; the flag returns the active game_id for that customer or null if the customer is not part of any active game.

5. Event and eligibility model

5.1 Source rails

5.2 Canonical event contract

{
  "event_id": "uuid-or-stable-source-id",
  "event_fingerprint": "deterministic-hash",
  "source_system": "UPI | FS",
  "event_type": "PAYMENT_SUCCESS | FS_ACTION_COMPLETE | PAYMENT_REVERSED",
  "txn_identity": "string",
  "cross_system_ref": {
    "ref_type": "UPI_RRN_IN_FS | FS_TXN_ID_IN_UPI",
    "ref_value": "string"
  },
  "user_id": "string",
  "customer_hash": "string",
  "event_time": "ISO-8601",
  "amount": 1234.56,
  "currency": "INR",
  "attributes": {
    "action_type": "CREDIT_CARD_APPLY"
  }
}

If upstream systems provide a stable immutable source identifier, it is used as event_id. Otherwise the adapter derives a deterministic event_fingerprint.

5.3 Eligibility flag contract

The gamification backend evaluates one static multivariate eligibility flag using customer_hash. The flag returns:

Phase 1 assumes upstream cohort governance and flag targeting guarantee at most one active game_id per customer at evaluation time. The flag is the enrollment gate — when it returns a game_id for a user with no existing enrollment, enrollment is created inline. The enrollment record then becomes the authoritative state of whether a user is enrolled; it is never modified by subsequent flag evaluations.

6. Config and game model

Games are stored as versioned JSON definitions in Postgres. The LD flag is the sole source of truth for cohort membership - there is no separate cohort definition stored on the game record and no refresh cadence. Once a user is enrolled (inline, on first qualifying event), they remain enrolled for the game window. FRM integration is deferred - see open issues.

{
  "schema_version": "1.0",
  "game_id": "upi_fs_milestone_may_2026",
  "config_version": 1,
  "status": "ACTIVE",
  "game_archetype": "LINEAR_MILESTONE",
  "game_window": {
    "start": "2026-05-01T00:00:00Z",
    "end": "2026-05-31T23:59:59Z"
  },
  "progress_state_model": {
    "type": "NUMERIC_COUNTER",
    "unit": "count",
    "max_value": 12
  },
  "milestone_steps": [
    { "step_id": "ms_3", "step": 3, "reward_id": "nc_10" },
    { "step_id": "ms_6", "step": 6, "reward_id": "nc_30" },
    { "step_id": "ms_12", "step": 12, "reward_id": "nc_100" }
  ],  
  "trigger_resolution": {
    "mode": "best_match_only",
    "best_match_rule": "highest_progress_value"
  },
  "triggers": [
    {
      "trigger_id": "upi_plus_one",
      "event_type": "PAYMENT_SUCCESS",
      "progression_config": { "mode": "increment_by_constant", "value": 1 }
    },
    {
      "trigger_id": "fs_plus_three",
      "event_type": "FS_ACTION_COMPLETE",
      "progression_config": { "mode": "increment_by_constant", "value": 3 }
    }
  ]
}

7. Write path

7.1 Progression path

[Diagram]

The LD flag is evaluated on every incoming event and is the sole enrollment gate. There is no separate enrollment trigger, batch seed, or refresh-driven enrollment path. When the flag returns a game_id and no enrollment exists, enrollment is created inline before progression is applied. Progress is applied only for events at or after enrolled_at, enforcing the no-backdating rule.

7.2 Arbitration

Arbitration applies only when an event carries a usable cross-system correlation marker. If present, the event enters a 5-minute coalescing window so paired UPI and FS representations can be resolved before progression. The default precedence is FS over UPI, and losing events are suppressed but retained in audit.

Open product question: when an event enters the coalescing window, should the post-transaction UX wait for arbitration completion before showing progress, or show provisional progress and later reconcile if needed?

8. Idempotency and replay

Stage Idempotency key Store
Ingestion event_id else event_fingerprint Redis + Postgres fallback
Arbitration txn_identity Postgres coalesce table
Enrollment game_instance_id:user_id Postgres unique constraint
Progress game_instance_id:event_id_or_fingerprint Redis + Postgres fallback
Reward dispatch reward_entitlement_id + provider key Postgres
Notification publish deterministic notification key side_effects_outbox
Analytics publish deterministic analytics key side_effects_outbox

Progress updates are monotonic and do not depend on event ordering. Late events may still be applied if they are within the game window, pass idempotency checks, and occur at or after enrollment time. Replay support must allow suppression of external side effects such as reward dispatch or notifications during recovery runs.

9. State model

Core entities are:

Additional enrollment fields on user_game_counter:

Field Type Notes
enrolled_at timestamp Lower bound for eligible progress evaluation; set to event time of first qualifying event

10. Rewards

[Diagram]

When a milestone is reached, the Progress Engine writes a reward_outbox record referencing the reward_entitlement. The Reward Orchestrator consumes it, looks up the reward_definition for the milestone's reward_id, and routes to the correct provider:

Provider idempotency keys are critical on both providers. The Reward Orchestrator retries on transient failures using exponential backoff with jitter. Without provider-side idempotency support, a retry after a network timeout would result in double disbursement — the orchestrator cannot distinguish a failed call from a succeeded-but-unacknowledged one. Entitlement is confirmed in Postgres only after the provider acknowledges success. Terminal failures route to reward DLQ and page on-call.

The reward_entitlement state machine is intentionally open for extension — future FRM hold states (HELD_FOR_REVIEW, FRM_FLAGGED) and clawback transitions are addable without schema changes.

11. Read path and Firestore

The authoritative write model and read model remain separate. Postgres write tables are the system of record, optimized for transactional correctness. The BFF never reads directly from write tables — instead, the Projection Writer listens to committed state changes from the Progress Engine and pre-computes denormalized, read-friendly snapshots into projection views, backed by a Redis cache for low-latency access. This keeps BFF reads fast and simple regardless of write table complexity.

Representative projections:

Firestore delivery: post-transaction game state payload

Firestore carries the full game state payload for the post-transaction screen directly. When the app's Firestore listener fires, it renders updated progress and any milestone reward immediately — no BFF round-trip required.

The Projection Writer calls the Firestore SDK (async, fire-and-forget) immediately after committing the Postgres state change. Reward details (reward_type, reward_value) are static game configuration known at the moment a milestone is crossed — they do not require waiting for actual disbursement. A single write is sufficient.

If the Firestore write fails, the app falls back to polling the BFF on a short timeout. The BFF (Redis → Postgres projection) is the authoritative fallback and is also used for the game journey page, full timeline, and background refresh.

[Diagram]

12. Notifications

The platform emits notification business events after authoritative state changes are committed. Phase 1 notification trigger types are:

The Scheduled Job Worker runs on a configurable cadence (e.g. every 30 minutes), queries user_game_counter, and writes matching records to side_effects_outbox with a day-bucketed idempotency key (e.g. reengagement:{game_id}:{user_id}:{date}) to prevent duplicate triggers within the same window.

The notification provider owns notification copy, timing, personalization, and deep links to the Game Journey Page. The backend only publishes trigger facts and payload attributes required for campaign orchestration. The specific notification platform and event contract are open items.

[Diagram]

13. Analytics instrumentation

Analytics is split into client interaction telemetry and backend business events. game_viewed is emitted from the app when the Game Journey Page opens. All other game analytics events are emitted from backend outbox processing only after the underlying state transition is committed.

Event Trigger Required properties
game_viewed User opens Game Journey Page user_id, game_id, timestamp, current_step
game_enrolled User enrolled inline on first qualifying event user_id, game_id, timestamp
game_joined First qualifying progression event user_id, game_id, timestamp, transaction_id, from_step, to_step
game_progress_updated Counter increments user_id, game_id, timestamp, from_step, to_step, transaction_id
game_milestone_completed Reward step reached user_id, game_id, timestamp, step, reward_type, reward_value
game_reward_unlocked Reward disbursement confirmed user_id, game_id, timestamp, reward_type, reward_value
game_fs_progression_applied FS rail event increments the progress counter (standalone or via arbitration) user_id, game_id, timestamp, from_step, to_step, fs_action_type, source, arbitration_applied
game_completed Final step reached user_id, game_id, timestamp, final_step

All backend-published analytics events include deterministic analytics keys so retries and replays do not inflate funnel metrics.

14. Retry and DLQ policy

Retry and DLQ policies are aligned to the criticality of each outbox and consumer. reward_outbox is the highest-criticality path; any entry in the reward DLQ must page on-call immediately.

Consumer Outbox Retry policy Terminal failure
Ingestion adapters - Transient broker failures retry Ingestion DLQ
Arbitration worker - Staged backoff Arbitration DLQ
Progress Engine (inline enrollment) - Idempotent retry Enrollment DLQ
Firestore Writer - (best-effort, no outbox) None - fire-and-forget Dropped silently; BFF poll is app fallback
Reward Orchestrator reward_outbox Exponential backoff with jitter; strict attempt cap Reward DLQ - page immediately
Notification Publisher side_effects_outbox Transient publish failures retry; lenient policy Notification DLQ
Analytics Publisher side_effects_outbox Transient sink failures retry; lenient policy Analytics DLQ

15. Technology stack

Component Technology Rationale
Backend services Go Good fit for event consumers, APIs, and concurrent workers.
Event bus: FS Kafka Existing enterprise backbone.
Event bus: UPI Azure Event Hub Existing CDC channel for UPI events.
Eligibility flag LaunchDarkly External segment-based routing decision returning game_id or null.
State store Postgres Transactional source of truth and atomicity boundary.
Dedup + cache Redis Hot-path deduplication and low-latency caching.
Firestore Firestore Best-effort post-transaction wakeup channel; app re-fetches authoritative state from BFF.
Push / in-app Notification provider (TBD) Configurable campaigns, personalization, and deep linking. Contract is an open item.
Neucoin provider Neucoin reward provider (TBD) Neucoin credit API; requires idempotency key support. Contract is an open item.
Voucher / offer provider Voucher offer provider (TBD) Voucher issuance API; requires idempotency key support. Contract is an open item.
Analytics sink Existing analytics stack Receives versioned client and business events.

16. Product decision: persistent enrollment override

The platform supports two valid operating modes for eligibility handling after a user has already been enrolled:

Phase 1 leaves this as an explicit product decision. Principle 5 describes the default behaviour (strict flag-gated: the LD flag is evaluated on every event). The persistent enrollment override is the opt-in alternative — the backend enrollment record becomes the authoritative runtime gate instead of the flag.

17. Key risks and open issues

# Risk Severity Owner Status
1 txn_identity derivation for cross-system events is not finalized. High Payments + BillPay Open
2 No FRM integration contract exists. Architecture will need to absorb FRM flags (enrollment gate, reward hold, progression pause) in a future phase with no prior alignment on interface or protocol. High FRM + Gamification Open — deferred
3 5-minute coalescing window needs validation against actual BillPay lag. Medium BillPay Open
4 Product decision is required on waiting for arbitration vs provisional UX. High Product Open
5 FS topic support for an additional gamification consumer group is still an external dependency. High FS team Open
6 Neucoin reward provider idempotency-key support must be confirmed. Without it, retries risk double disbursement. High Neucoin provider + Gamification Open
6b Voucher offer provider idempotency-key support for voucher issuance API must be confirmed. Same double-disbursement risk on retry. High Voucher provider + Gamification Open
7 Static eligibility flag contract, context key, and freshness SLA must be finalized with the external segment platform. High Business + Segment Platform + Gamification Open
8 Product decision required on strict flag-gated processing vs persistent enrollment override. High Product Open
9 Notification event contract (trigger names, payload schema, customer identifier mapping, deep-link format) must be agreed before the Notification Publisher can be implemented. Medium CRM + Product + Gamification Open
10 Analytics schema governance and event versioning policy need agreement before launch. Medium Analytics + Product + Gamification Open
11 Neucoin reward provider API contract (endpoint, auth, request/response schema, user identifier mapping) is undefined. High Neucoin provider + Gamification Open
12 Voucher offer provider API contract (endpoint, auth, request/response schema, user identifier mapping) is undefined. High Voucher provider + Gamification Open

18. External dependency matrix

External team What they must provide Acceptance criterion
Payments / UPI UPI success + reversal events via Event Hub, optional FS cross-ref fields Events available in agreed format; consumer group registered
BillPay FS completion / skip / reversal events via Kafka, optional UPI RRN field Topic available; schema agreed; latency shared
FS Confirmation that gamification can consume via a separate consumer group Written confirmation of safe co-existence with existing consumers
Business + Segment Platform Cohort definition, Viduur-to-LD segment mapping, and one static eligibility flag returning game_id or null Contract agreed for context key, freshness, and single-active-game guarantee
Neucoin reward provider Neucoin credit API with idempotency key support Idempotency confirmed; staging available; API contract agreed
Voucher offer provider Voucher issuance API with idempotency key support Idempotency confirmed; staging available; API contract agreed
FRM Integration contract to be defined in a future phase. No Phase 1 dependency. -
Notification provider Event-triggered campaign configuration, personalization, and deep-link contract Event schema and deep-link contract approved in staging
Analytics Accepted event schema, sink contract, and versioning policy Tracking plan approved and validated