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 Mode | Contest Type | Entry Type | Settlement |
|---|---|---|---|
| Classic Fantasy | GPP, H2H, 50/50 | Team picks | Points-based ranking |
| Player Props | Slip contests | Prop selections | All-or-nothing / Insurance |
| Live Trading | Market sessions | Share positions | Price at close |
| Prediction Pools | Parimutuel pools | Yes/No picks | Pool 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
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/v3/contests | Create new contest | Admin |
| GET | /api/v3/contests | List contests with filters | Public |
| GET | /api/v3/contests/:id | Get contest details | Public |
| PATCH | /api/v3/contests/:id | Update contest config | Admin |
| POST | /api/v3/contests/:id/entries | Submit entry | User |
| GET | /api/v3/contests/:id/entries | List entries | Public |
| GET | /api/v3/contests/:id/leaderboard | Get live leaderboard | Public |
| POST | /api/v3/contests/:id/cancel | Cancel contest | Admin |
| GET | /api/v3/contests/:id/settlements | Get settlement details | User |
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
| Category | Examples | Handling Strategy |
|---|---|---|
| Transient | Network timeout, DB connection | Exponential backoff retry |
| Business | Contest full, invalid team | Return error to user |
| System | Kafka unavailable, Redis down | Circuit breaker + fallback |
| Data | Corrupted event, schema mismatch | Dead 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
- Detect Issue: Automated alerts or manual observation
- Switch Traffic: Route 100% to previous version
- Investigate: Analyze logs and traces
- Fix Forward: Deploy hotfix or revert code
- Post-Mortem: Document and prevent recurrence
17. Integration Points
17.1 Service Dependencies
| Service | Integration Type | Purpose |
|---|---|---|
| Scoring Manager | Kafka Events | Receive score updates |
| Pool Manager | gRPC | Manage prize pools |
| Auction Engine | Kafka Events | Coordinate auction contests |
| Wallet Service | gRPC | Handle payments |
| Notification Service | Kafka Events | Send user notifications |
| Match Service | REST/Webhook | Get match status |
17.2 Integration Diagram
18. Future Enhancements
18.1 Roadmap
| Phase | Feature | Description |
|---|---|---|
| Q1 2026 | Multi-match Contests | Contests spanning multiple matches |
| Q2 2026 | Private Leagues | User-created private contests |
| Q3 2026 | AI Recommendations | ML-powered team suggestions |
| Q4 2026 | Cross-platform Sync | Real-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
| Term | Definition |
|---|---|
| Contest | A competition where users submit fantasy teams |
| Entry | A user's participation in a contest with a team |
| Settlement | The process of calculating and distributing prizes |
| Tiebreaker | Rules to determine ranking when scores are equal |
19.2 References
- Dream11 Engineering Blog
- Apache Kafka Documentation
- Redis Cluster Specification
- CQRS Pattern - Microsoft
Document Version: 3.0.0 | Last Updated: January 2026