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.
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.
game_id or null for a customer hash.reward_entitlement records carry immutable status transitions and a stable idempotency key, making reversal and clawback processing addable without schema changes.game_id per customer.game_id and no enrollment record exists for that user and game instance.reward_outbox and side_effects_outbox - each tuned for its own retry policy, retention, and criticality level.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.
{
"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.
The gamification backend evaluates one static multivariate eligibility flag using customer_hash. The flag returns:
game_id when the customer belongs to one active game cohort.null when the customer is not eligible for any active game.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.
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 }
}
]
}
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.
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?
| 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.
Core entities are:
game_definitionreward_definition — stores reward_type (NEUCOIN | VOUCHER), provider (CAPILLARY | OFFER_ENGINE), and provider-specific fields (value for neucoins, offer_id for vouchers)user_game_instance / user_game_countergame_event_logreward_entitlement — carries reward_type, provider, and provider_idempotency_key; status transitions are immutableevent_processing_logreward_outbox - tracks reward dispatch intent; consumed exclusively by the Reward Orchestratorside_effects_outbox - tracks notification and analytics publication intent; consumed by Notification Publisher and Analytics PublisherAdditional 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 |
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:
value and a stable provider_idempotency_key (reward_entitlement_id). API contract is an open item (see open issues table).offer_id and the same idempotency key. API contract is an open item (see open issues table).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.
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:
user_game_summary - current step, next milestone, rewards unlocked; primary game dashboard feedgame_event_timeline - per-transaction history for the game journey pagetransaction_outcome - whether a specific transaction counted towards the gameuser_game_analytics_view - backend analytics read modelFirestore 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.
The platform emits notification business events after authoritative state changes are committed. Phase 1 notification trigger types are:
GAME_MILESTONE_ACHIEVED — emitted from side_effects_outbox immediately after a milestone commit.GAME_ENDING_SOON_REMINDER_ELIGIBLE — emitted by a Scheduled Job Worker that queries for enrolled users with incomplete progress when game_window.end is within 3 days.GAME_REENGAGEMENT_48H_ELIGIBLE — emitted by the same Scheduled Job Worker for enrolled users with no progression event in the last 48 hours.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.
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.
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 |
| 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. |
The platform supports two valid operating modes for eligibility handling after a user has already been enrolled:
null, future events are dropped for gamification.null.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.
| # | 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 |
| 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 |