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