Skip to main content

Contest Engine - Detailed Design Document

Architecture Version: 3.0 | Last Updated: January 2026 Pattern References: Dream11 Kafka Architecture, MPL Data Streaming, FanDuel Real-Time Analytics


1. Executive Overview

The Contest Engine is the central orchestration layer of the bOS Gaming Platform, responsible for managing the complete lifecycle of contests across all four game modes: Classic Fantasy, Player Props, Live Trading, and Prediction Pools. Drawing from production patterns used by Dream11 (230M+ users) and MPL, this design implements an event-driven architecture using Apache Kafka for real-time data streaming, ensuring sub-second contest updates at massive scale.

Think of the Contest Engine like a sophisticated tournament management system: It doesn't just run one tournament—it simultaneously manages thousands of contests across multiple sports and game modes, each with different rules, prize structures, and participant pools, all updating in real-time as live matches progress.

1.1 Multi-Mode Support

The Contest Engine is designed as a game-mode-agnostic orchestration layer that delegates mode-specific logic to specialized engines:

Game ModeContest TypeEntry TypeSettlement
Classic FantasyGPP, H2H, 50/50Team picksPoints-based ranking
Player PropsSlip contestsProp selectionsAll-or-nothing / Insurance
Live TradingMarket sessionsShare positionsPrice at close
Prediction PoolsParimutuel poolsYes/No picksPool share

2. Core Architecture Principles

2.1 Event-Driven Design (EDA)

The Contest Engine follows an event-driven architecture pattern, where all state changes are captured as immutable events. This provides:

  • Guaranteed ordering of contest operations
  • Durability through event log persistence
  • Replayability for debugging and recovery
  • Decoupled services that scale independently

2.2 CQRS Pattern (Command Query Responsibility Segregation)

The Contest Engine separates write operations (commands) from read operations (queries) for optimal performance at scale.


3. System Components

3.1 Component Architecture


4. Contest Lifecycle State Machine


5. Data Models

5.1 Entity Relationship Diagram

5.2 Core TypeScript Interfaces

// Contest Types
interface Contest {
id: string;
name: string;
status: ContestStatus;
matchId: string;
entryFee: number;
maxEntries: number;
currentEntries: number;
entryOpens: Date;
entryCloses: Date;
config: ContestConfig;
prizeStructure: PrizeStructure;
createdAt: Date;
updatedAt: Date;
}

type ContestStatus =
| 'DRAFT'
| 'SCHEDULED'
| 'OPEN'
| 'LOCKED'
| 'LIVE'
| 'CALCULATING'
| 'SETTLED'
| 'CANCELLED';

// Game Mode Types
type GameMode = 'CLASSIC_FANTASY' | 'PLAYER_PROPS' | 'LIVE_TRADING' | 'PREDICTION_POOLS';

interface ContestConfig {
gameMode: GameMode;
sportType: SportType;
contestType: ContestType;
// Classic Fantasy specific
teamSize?: number;
captainMultiplier?: number;
viceCaptainMultiplier?: number;
// Player Props specific
minLegs?: number;
maxLegs?: number;
insuranceAvailable?: boolean;
// Live Trading specific
ammConfig?: AMMConfig;
tradingWindow?: TradingWindow;
// Prediction Pools specific
poolType?: 'BINARY' | 'MULTI_OUTCOME';
// Common
maxEntriesPerUser: number;
minEntriesToStart: number;
allowLateJoin: boolean;
tiebreaker: TiebreakerRule[];
}

interface AMMConfig {
initialLiquidity: number;
feePercentage: number;
priceFloor: number;
priceCeiling: number;
}

interface TradingWindow {
opensAt: Date;
closesAt: Date;
allowLiveTrading: boolean;
}

interface ContestEntry {
id: string;
contestId: string;
userId: string;
teamId: string;
currentScore: number;
currentRank: number;
status: EntryStatus;
submittedAt: Date;
prizeWon?: number;
}

interface PrizeStructure {
totalPool: number;
platformFeePct: number;
rankPayouts: RankPayout[];
guaranteedPool: boolean;
}

interface RankPayout {
rankFrom: number;
rankTo: number;
amount: number;
percentage?: number;
}

6. Event Schemas

6.1 Kafka Topic Structure

fantasy.contests.commands     # Inbound commands
fantasy.contests.events # State change events
fantasy.contests.entries # Entry submissions
fantasy.contests.scores # Score updates
fantasy.contests.settlements # Prize distributions

6.2 Event Definitions

// Base Event Structure
interface ContestEvent {
eventId: string;
eventType: string;
aggregateId: string; // contestId
timestamp: Date;
version: number;
payload: unknown;
metadata: EventMetadata;
}

// Contest Created Event
interface ContestCreatedEvent extends ContestEvent {
eventType: 'CONTEST_CREATED';
payload: {
contestId: string;
matchId: string;
name: string;
entryFee: number;
maxEntries: number;
config: ContestConfig;
prizeStructure: PrizeStructure;
};
}

// Entry Submitted Event
interface EntrySubmittedEvent extends ContestEvent {
eventType: 'ENTRY_SUBMITTED';
payload: {
entryId: string;
contestId: string;
userId: string;
teamId: string;
entryNumber: number;
timestamp: Date;
};
}

// Contest State Changed Event
interface ContestStateChangedEvent extends ContestEvent {
eventType: 'CONTEST_STATE_CHANGED';
payload: {
contestId: string;
previousState: ContestStatus;
newState: ContestStatus;
reason: string;
triggeredBy: string;
};
}

// Score Updated Event
interface ScoreUpdatedEvent extends ContestEvent {
eventType: 'SCORE_UPDATED';
payload: {
contestId: string;
entryId: string;
previousScore: number;
newScore: number;
previousRank: number;
newRank: number;
scoringEventId: string;
};
}

// Prize Distributed Event
interface PrizeDistributedEvent extends ContestEvent {
eventType: 'PRIZE_DISTRIBUTED';
payload: {
contestId: string;
distributions: PrizeDistribution[];
totalDistributed: number;
platformFee: number;
};
}

7. API Specifications

7.1 REST API Endpoints

MethodEndpointDescriptionAuth
POST/api/v3/contestsCreate new contestAdmin
GET/api/v3/contestsList contests with filtersPublic
GET/api/v3/contests/:idGet contest detailsPublic
PATCH/api/v3/contests/:idUpdate contest configAdmin
POST/api/v3/contests/:id/entriesSubmit entryUser
GET/api/v3/contests/:id/entriesList entriesPublic
GET/api/v3/contests/:id/leaderboardGet live leaderboardPublic
POST/api/v3/contests/:id/cancelCancel contestAdmin
GET/api/v3/contests/:id/settlementsGet settlement detailsUser

7.2 gRPC Service Definition

service ContestService {
// Contest Management
rpc CreateContest(CreateContestRequest) returns (Contest);
rpc GetContest(GetContestRequest) returns (Contest);
rpc UpdateContest(UpdateContestRequest) returns (Contest);
rpc ListContests(ListContestsRequest) returns (ListContestsResponse);

// Entry Management
rpc SubmitEntry(SubmitEntryRequest) returns (ContestEntry);
rpc WithdrawEntry(WithdrawEntryRequest) returns (WithdrawEntryResponse);
rpc GetEntry(GetEntryRequest) returns (ContestEntry);
rpc ListUserEntries(ListUserEntriesRequest) returns (ListEntriesResponse);

// Real-time Streaming
rpc StreamLeaderboard(StreamLeaderboardRequest) returns (stream LeaderboardUpdate);
rpc StreamContestStatus(StreamContestStatusRequest) returns (stream ContestStatusUpdate);

// Settlement
rpc GetSettlement(GetSettlementRequest) returns (Settlement);
}

message CreateContestRequest {
string match_id = 1;
string name = 2;
int32 entry_fee = 3;
int32 max_entries = 4;
ContestConfig config = 5;
PrizeStructure prize_structure = 6;
}

message SubmitEntryRequest {
string contest_id = 1;
string team_id = 2;
string idempotency_key = 3; // Prevents duplicate submissions
}

message LeaderboardUpdate {
string contest_id = 1;
int64 timestamp = 2;
repeated LeaderboardEntry entries = 3;
bool is_final = 4;
}

8. Entry Processing Pipeline

8.1 Entry Submission Flow

8.2 Validation Rules

class EntryValidator {
async validate(request: SubmitEntryRequest): Promise<ValidationResult> {
const rules: ValidationRule[] = [
new ContestStatusRule(), // Contest must be OPEN
new EntryWindowRule(), // Within entry window
new TeamCompositionRule(), // Valid team structure
new CreditLimitRule(), // Within credit budget
new UserEntryLimitRule(), // Max entries per user
new ContestCapacityRule(), // Contest not full
new DuplicateTeamRule(), // No duplicate teams
new PlayerAvailabilityRule(), // All players available
];

for (const rule of rules) {
const result = await rule.check(request);
if (!result.valid) {
return result;
}
}

return { valid: true };
}
}

9. Concurrency Control

9.1 Distributed Locking Strategy

The Contest Engine uses Redis-based distributed locks to handle concurrent operations safely. This is critical during high-traffic periods like match start times when thousands of users submit entries simultaneously.

9.2 Optimistic Concurrency with Version Vectors

class ContestRepository {
async updateWithOptimisticLock(
contestId: string,
expectedVersion: number,
updates: Partial<Contest>
): Promise<Contest> {
const result = await this.db.query(`
UPDATE contests
SET
${this.buildUpdateClause(updates)},
version = version + 1,
updated_at = NOW()
WHERE id = $1 AND version = $2
RETURNING *
`, [contestId, expectedVersion]);

if (result.rowCount === 0) {
throw new OptimisticLockError(
`Contest ${contestId} was modified by another process`
);
}

return result.rows[0];
}
}

9.3 Entry Count Management with Redis Atomic Operations

class EntryCountManager {
private redis: Redis;

async reserveEntrySlot(contestId: string, maxEntries: number): Promise<boolean> {
const key = `contest:${contestId}:entry_count`;

// Atomic increment with limit check using Lua script
const script = `
local current = redis.call('GET', KEYS[1])
if current == false then current = 0 else current = tonumber(current) end
if current >= tonumber(ARGV[1]) then
return -1 -- Contest full
end
return redis.call('INCR', KEYS[1])
`;

const result = await this.redis.eval(script, 1, key, maxEntries);
return result !== -1;
}

async releaseEntrySlot(contestId: string): Promise<void> {
const key = `contest:${contestId}:entry_count`;
await this.redis.decr(key);
}
}

10. Prize Distribution Engine

10.1 Settlement Flow

10.2 Tiebreaker Implementation

class TiebreakerEngine {
private rules: TiebreakerRule[] = [
new HigherCaptainScoreRule(),
new HigherViceCaptainScoreRule(),
new FewerPlayersUsedRule(),
new EarlierSubmissionRule(),
];

resolveTies(entries: RankedEntry[]): RankedEntry[] {
// Group entries by score
const scoreGroups = this.groupByScore(entries);

const resolved: RankedEntry[] = [];
let currentRank = 1;

for (const group of scoreGroups) {
if (group.length === 1) {
group[0].rank = currentRank;
resolved.push(group[0]);
} else {
// Apply tiebreakers sequentially
const sorted = this.applyTiebreakers(group);
for (const entry of sorted) {
entry.rank = currentRank;
resolved.push(entry);
}
}
currentRank += group.length;
}

return resolved;
}

private applyTiebreakers(entries: RankedEntry[]): RankedEntry[] {
let remaining = [...entries];

for (const rule of this.rules) {
if (remaining.length <= 1) break;
remaining = rule.apply(remaining);
}

return remaining;
}
}

11. Scalability Patterns

11.1 Horizontal Scaling Architecture

11.2 Kafka Partition Strategy

// Partition by contest_id for ordered processing per contest
class ContestPartitioner implements Partitioner {
partition(
topic: string,
key: string | null,
message: Buffer,
metadata: PartitionMetadata
): number {
if (!key) {
// Random partition for keyless messages
return Math.floor(Math.random() * metadata.partitionCount);
}

// Hash contest_id to ensure all events for a contest
// go to the same partition (ordered processing)
const hash = this.murmurHash(key);
return Math.abs(hash) % metadata.partitionCount;
}
}

// Topic configuration for high throughput
const topicConfig = {
'fantasy.contests.entries': {
partitions: 32, // High parallelism for entries
replicationFactor: 3, // Durability
retentionMs: 7 * 24 * 60 * 60 * 1000, // 7 days
cleanupPolicy: 'delete',
},
'fantasy.contests.events': {
partitions: 16,
replicationFactor: 3,
retentionMs: 30 * 24 * 60 * 60 * 1000, // 30 days for audit
cleanupPolicy: 'compact', // Keep latest state
},
};

11.3 Auto-Scaling Configuration

# Kubernetes HPA for Contest Engine
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: contest-engine-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: contest-engine
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: kafka_consumer_lag
target:
type: AverageValue
averageValue: "1000" # Scale up if lag > 1000
- type: External
external:
metric:
name: active_contests
target:
type: Value
value: "100" # Scale based on active contests

12. Monitoring & Observability

12.1 Key Metrics Dashboard

12.2 Critical Metrics

// Prometheus metrics for Contest Engine
const metrics = {
// Entry Processing
entrySubmissionTotal: new Counter({
name: 'contest_entry_submissions_total',
help: 'Total entry submissions',
labelNames: ['contest_type', 'status'],
}),

entryProcessingDuration: new Histogram({
name: 'contest_entry_processing_seconds',
help: 'Entry processing duration',
labelNames: ['contest_type'],
buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
}),

// Contest State
activeContests: new Gauge({
name: 'contest_active_total',
help: 'Number of active contests',
labelNames: ['status', 'sport_type'],
}),

// Kafka Consumer
kafkaConsumerLag: new Gauge({
name: 'contest_kafka_consumer_lag',
help: 'Kafka consumer lag',
labelNames: ['topic', 'partition'],
}),

// Settlement
settlementDuration: new Histogram({
name: 'contest_settlement_seconds',
help: 'Contest settlement duration',
labelNames: ['contest_type'],
buckets: [1, 5, 10, 30, 60, 120, 300],
}),
};

12.3 Alert Rules

groups:
- name: contest-engine-alerts
rules:
- alert: HighEntryProcessingLatency
expr: histogram_quantile(0.99, contest_entry_processing_seconds) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "Entry processing p99 latency > 2s"

- alert: KafkaConsumerLagHigh
expr: contest_kafka_consumer_lag > 10000
for: 2m
labels:
severity: critical
annotations:
summary: "Kafka consumer lag exceeds 10k messages"

- alert: ContestSettlementStuck
expr: contest_status{status="CALCULATING"} > 0
and time() - contest_status_changed_timestamp > 3600
for: 5m
labels:
severity: critical
annotations:
summary: "Contest stuck in CALCULATING state for > 1 hour"

13. Error Handling & Recovery

13.1 Error Categories

CategoryExamplesHandling Strategy
TransientNetwork timeout, DB connectionExponential backoff retry
BusinessContest full, invalid teamReturn error to user
SystemKafka unavailable, Redis downCircuit breaker + fallback
DataCorrupted event, schema mismatchDead letter queue + alert

13.2 Dead Letter Queue Pattern

13.3 Circuit Breaker Implementation

class ContestServiceCircuitBreaker {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private lastFailureTime: Date | null = null;

private readonly config = {
failureThreshold: 5,
recoveryTimeout: 30000, // 30 seconds
halfOpenRequests: 3,
};

async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (this.shouldAttemptRecovery()) {
this.state = 'HALF_OPEN';
} else {
throw new CircuitOpenError('Service temporarily unavailable');
}
}

try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}

private onSuccess(): void {
this.failureCount = 0;
this.state = 'CLOSED';
}

private onFailure(): void {
this.failureCount++;
this.lastFailureTime = new Date();

if (this.failureCount >= this.config.failureThreshold) {
this.state = 'OPEN';
}
}

private shouldAttemptRecovery(): boolean {
if (!this.lastFailureTime) return true;
const elapsed = Date.now() - this.lastFailureTime.getTime();
return elapsed >= this.config.recoveryTimeout;
}
}

14. Security Considerations

14.1 Authentication & Authorization

14.2 Rate Limiting

const rateLimitConfig = {
// Entry submission - prevent abuse
'POST /contests/:id/entries': {
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 entries per minute per user
keyGenerator: (req) => `${req.user.id}:${req.params.id}`,
},

// Leaderboard polling - prevent DDoS
'GET /contests/:id/leaderboard': {
windowMs: 1000, // 1 second
max: 5, // 5 requests per second
keyGenerator: (req) => req.ip,
},

// Admin operations - strict limits
'POST /contests': {
windowMs: 60 * 1000,
max: 100,
keyGenerator: (req) => req.user.id,
},
};

15. Testing Strategy

15.1 Test Pyramid

15.2 Key Test Scenarios

describe('ContestEngine', () => {
describe('Entry Submission', () => {
it('should accept valid entry within limits', async () => {
// Test happy path
});

it('should reject entry when contest is full', async () => {
// Test capacity limit
});

it('should handle concurrent entries atomically', async () => {
// Test race condition handling
});

it('should rollback on wallet failure', async () => {
// Test saga compensation
});
});

describe('State Transitions', () => {
it('should transition OPEN -> LOCKED at entry close time', async () => {
// Test scheduled transition
});

it('should cancel contest with insufficient entries', async () => {
// Test minimum entry rule
});
});

describe('Settlement', () => {
it('should distribute prizes according to structure', async () => {
// Test prize calculation
});

it('should handle ties with tiebreaker rules', async () => {
// Test tiebreaker logic
});
});
});

16. Deployment & Operations

16.1 Deployment Strategy

16.2 Rollback Procedure

  1. Detect Issue: Automated alerts or manual observation
  2. Switch Traffic: Route 100% to previous version
  3. Investigate: Analyze logs and traces
  4. Fix Forward: Deploy hotfix or revert code
  5. Post-Mortem: Document and prevent recurrence

17. Integration Points

17.1 Service Dependencies

ServiceIntegration TypePurpose
Scoring ManagerKafka EventsReceive score updates
Pool ManagergRPCManage prize pools
Auction EngineKafka EventsCoordinate auction contests
Wallet ServicegRPCHandle payments
Notification ServiceKafka EventsSend user notifications
Match ServiceREST/WebhookGet match status

17.2 Integration Diagram


18. Future Enhancements

18.1 Roadmap

PhaseFeatureDescription
Q1 2026Multi-match ContestsContests spanning multiple matches
Q2 2026Private LeaguesUser-created private contests
Q3 2026AI RecommendationsML-powered team suggestions
Q4 2026Cross-platform SyncReal-time sync across devices

18.2 Technical Debt

  • Migrate from REST to gRPC for internal services
  • Implement event sourcing for full audit trail
  • Add GraphQL subscriptions for real-time updates
  • Optimize leaderboard queries with materialized views

19. Appendix

19.1 Glossary

TermDefinition
ContestA competition where users submit fantasy teams
EntryA user's participation in a contest with a team
SettlementThe process of calculating and distributing prizes
TiebreakerRules to determine ranking when scores are equal

19.2 References


Document Version: 3.0.0 | Last Updated: January 2026