Skip to main content

Table overview

All persistent storage lives in DynamoDB. There are no relational databases. Each table is owned by a single repository, though multiple services may read from it via that repository.

Table details

cricket-fixtures

The central table. Stores every match the system knows about.
KeyTypeDescription
matchId (PK)StringSportMonks fixture ID
GSIs:
GSI namePartition keySort keyPurpose
leagueCode-startTime-indexleagueCodestartTimeQuery fixtures by league, sorted by start time
live-indexlivestartTimeFind all currently live matches
Accessed by: FixtureService (read/write), FixtureSyncJob (write), DisplayStateService (read) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get fixture by IDBothGetItemPK=matchIdGET /matches/:id, LiveScoreJobPer request / every 2.5s per live match
Batch get fixturesBothBatchGetItemPK=matchId (up to 100)GET /matches, FixtureCacheServicePer request / per sync cycle
Get matches by date rangeUserQuery (GSI)leagueCode-startTime-index, leagueCode = :code AND startTime BETWEEN :from AND :toGET /matches?league=&from=&to=Per user request
Get starting-soon matchesSystemQuery (GSI)leagueCode-startTime-index, startTime BETWEEN :now AND :until with filter attribute_not_exists(liveUpdatedAt) AND attribute_not_exists(archivedAt)PrematchNotificationJobEvery 60s
Scan live fixturesSystemScan (GSI)live-index full scanLiveScoreJob reconciliationEvery 2.5s
Batch write fixturesSystemBatchWriteItemPK=matchId (batches of 25)FixtureSyncJobPer sync cycle
Update fixture fieldsSystemUpdateItemPK=matchId with ConditionExpression: attribute_exists(matchId)LiveScoreJob, FixtureSyncJobPer live match per tick
Archive fixtureSystemUpdateItemPK=matchId, SETs archivedAt, REMOVEs liveUpdatedAtLiveScoreJob reconciliationWhen match leaves livescores
Delete replay fixtureSystemDeleteItemPK=matchIdLiveScoreJob reconciliationWhen replay expires
Get fixtures by league (admin)UserScan + filterFilterExpression: leagueId = :lidAdmin endpointsRare, admin-only
The live-index GSI is sparse — only fixtures with liveUpdatedAt set appear in it. The poller SETs liveUpdatedAt on every tick for live matches and REMOVEs it during archival, which drops the fixture from the GSI. At peak, the GSI holds 10-20 items, making a full Scan cost less than 1 RCU.
The archiveFixture operation uses a conditional write (attribute_exists(matchId)) to prevent accidental upserts. It also REMOVEs liveUpdatedAt in the same UpdateExpression, which atomically drops the fixture from the sparse GSI.

cricket-teams

Static team reference data synced from SportMonks.
KeyTypeDescription
teamId (PK)StringSportMonks team ID
Accessed by: TeamService (read/write), FixtureEnrichmentProvider (read) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get team by IDBothGetItemPK=teamIdGET /teams/:id, FixtureEnrichmentProviderPer request / per enrichment
Get all teamsUserScanFull table scan (< 100 items)GET /teamsPer request, cached
Batch write teamsSystemBatchWriteItemPK=teamId (batches of 25)TeamSyncJobPer sync cycle
Update team fieldsUserUpdateItemPK=teamId with ConditionExpression: attribute_exists(teamId)PATCH /teams/:id (admin)Rare, admin-only

cricket-team-seasons

Per-season stats for each team within a league season.
KeyTypeDescription
seasonId (PK)StringSportMonks season ID
teamId (SK)StringSportMonks team ID
Accessed by: TeamSeasonService (read/write) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get teams for a seasonBothQueryPK=seasonIdGET /seasons/:id/teams, FixtureEnrichmentProviderPer request / per enrichment
Write season-team recordsSystemBatchWriteItemPK=seasonId, SK=teamId (batches of 25)TeamSyncJobPer sync cycle

cricket-leagues

League/competition reference data.
KeyTypeDescription
leagueId (PK)StringSportMonks league ID
Accessed by: LeagueService (read/write), FixtureEnrichmentProvider (read) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get league by IDBothGetItemPK=leagueIdGET /leagues/:id, FixtureEnrichmentProviderPer request / per enrichment
Get all leaguesUserScanFull table scan (< 30 items)GET /leaguesPer request, cached in-memory
Get active leaguesSystemScan + filterFilterExpression: active = truePrematchNotificationJob, FixtureSyncJobPer job cycle, cached
Create/replace leagueSystemPutItemPK=leagueIdLeagueSyncJobPer sync cycle
Update league fieldsUserUpdateItemPK=leagueId with ConditionExpression: attribute_exists(leagueId)PATCH /leagues/:id (admin)Rare, admin-only
Delete leagueUserDeleteItemPK=leagueIdAdmin endpointRare

cricket-devices

Maps users to their registered push notification device tokens.
KeyTypeDescription
userId (PK)StringCognito user ID
deviceToken (SK)StringAPNs device token
TTL: Yes — stale device registrations expire automatically. Accessed by: DeviceService (read/write), PushNotificationService (read) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get device for userBothQueryPK=userId, Limit: 1GET /user/device, PushNotificationServicePer push send / per request
Register device (upsert)UserQuery + Delete + UpdateItemQuery PK=userId to find stale records, Delete orphans, then UpdateItem PK=userId, SK=deviceTokenPOST /user/deviceOn app launch
Update push-to-start tokenUserGetItem + UpdateItemPK=userId, SK=deviceTokenPUT /user/device/push-to-startOn token refresh
Delete deviceUserDeleteItemPK=userId, SK=deviceToken with ReturnValues: ALL_OLDDELETE /user/deviceUser action
Device registration enforces single-device-per-user: it queries all records for the user, deletes any with a different deviceToken, then upserts the current one. This is a multi-step operation (not transactional) but safe because each user only ever has 1-2 records.

cricket-match-follows

Tracks which users are following which matches (for push notifications on score changes).
KeyTypeDescription
matchId (PK)StringFixture match ID
userId (SK)StringCognito user ID
Accessed by: MatchFollowService (read/write), PushNotificationService (read) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Check if user follows matchUserGetItemPK=matchId, SK=userIdGET /user/follow/:matchIdPer request
Get all followers for matchSystemQueryPK=matchId (paginated)TossDetectionJob, LiveActivityPushJobEvery 2.5s per live match
Follow a matchUserUpdateItemPK=matchId, SK=userId, SET teamId, expiresAt, createdAt = if_not_exists(...)POST /user/follow/:matchIdUser action
Unfollow a matchUserDeleteItemPK=matchId, SK=userId with ReturnValues: ALL_OLDDELETE /user/follow/:matchIdUser action
Update follow teamUserUpdateItemPK=matchId, SK=userId with ConditionExpression: attribute_exists(matchId)PATCH /user/follow/:matchIdUser action
Remove all follows for matchSystemBatchDeleteItemQuery PK=matchId, then batch delete allMatch completion cleanupOnce per match end
updateFollowTeam uses a conditional write (attribute_exists(matchId)) to prevent creating a follow record if one doesn’t already exist. Returns false on ConditionalCheckFailedException.
expiresAt is set to match start time + 48h buffer. DynamoDB TTL automatically cleans up follow records for completed matches — no cron job needed.

cricket-activity-subscriptions

Tracks Live Activity subscriptions. When a user starts a Live Activity on iOS, their push token for that activity is stored here.
KeyTypeDescription
matchId (PK)StringFixture match ID
userId (SK)StringCognito user ID
TTL: Yes — subscriptions expire after the match ends. Accessed by: ActivitySubscriptionService (read/write), LiveActivityPushService (read) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get all subscriptions for matchSystemQueryPK=matchId (paginated)LiveActivityPushJobEvery 2.5s per live match
Register LA subscriptionUserUpdateItem (upsert)PK=matchId, SK=pushToken, SET userId, teamId, expiresAt, createdAt = if_not_exists(...)POST /user/activity/:matchIdWhen iOS starts Live Activity
Remove single subscriptionUserDeleteItemPK=matchId, SK=pushTokenDELETE /user/activity/:matchIdWhen iOS ends Live Activity
Remove user’s subscriptions for matchUserQuery + DeleteQuery PK=matchId with FilterExpression: userId = :userId, then delete eachUnfollow cascadeOn unfollow
Remove all subscriptions for matchSystemBatchDeleteItemQuery PK=matchId, then batch delete allMatch completion cleanupOnce per match end
The SK is pushToken (the Live Activity push token), not userId. This enables multi-device support — a user with iPhone + iPad has 2 separate subscription records for the same match.
Live Activity push tokens are different from regular APNs tokens. They are short-lived and tied to a specific activity instance, not the device.

cricket-prematch-notifications

Tracks whether a prematch reminder has been sent for a user-match pair. Prevents duplicate notifications.
KeyTypeDescription
matchId (PK)StringFixture match ID
userId (SK)StringCognito user ID
Accessed by: PrematchNotificationService (read/write) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Check if notification sentSystemGetItemPK=matchIdPrematchNotificationJobPer match approaching start
Mark notification sentSystemPutItemPK=matchId, with 48h TTLPrematchNotificationJobOnce per match
This table is system-only — no user-facing API touches it. TTL is set to 48 hours after write, so records auto-expire without cleanup.

cricket-locks

Distributed locking table. Prevents multiple backend instances from processing the same match concurrently.
KeyTypeDescription
lockKey (PK)StringLock identifier (usually match:{matchId})
Key attributes: owner (instance ID), expiresAt (epoch timestamp for auto-release) Accessed by: LockService (read/write) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Acquire lockSystemPutItem (conditional)PK=lockId with ConditionExpression: attribute_not_exists(lockId) OR expiresAt < :nowLiveScoreJob, any concurrent jobEvery 2.5s per live match
Release lockSystemDeleteItem (conditional)PK=lockId with ConditionExpression: holder = :holderLiveScoreJob completionAfter each processing tick
Extend lockSystemUpdateItem (conditional)PK=lockId with ConditionExpression: holder = :holderLong-running jobsDuring processing
All three operations use conditional writes for atomicity. ConditionalCheckFailedException on acquire means another instance holds the lock — this is expected behavior, not an error. Default TTL is 10 seconds, acting as an auto-release safety net if the holder crashes.

cricket-state-hashes

Stores a hash fingerprint of the last-pushed match state. Used to detect whether a match state has actually changed before sending pushes.
KeyTypeDescription
matchId (PK)StringFixture match ID
Accessed by: StateHashService (read/write), DisplayStateService (read) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get stored hashSystemGetItemPK=matchTeamKey (composite: {matchId}#{teamId})LiveScoreJob change detectionEvery 2.5s per live match per team
Update stored hashSystemPutItemPK=matchTeamKeyLiveScoreJob (when state changed)On each state change
Delete hashSystemDeleteItemPK=matchTeamKeyMatch completion cleanupOnce per match end
The PK is a composite key {matchId}#{teamId} — hashes are stored per-team so that neutral vs team-perspective pushes can be independently deduplicated. This table is system-only; no user API reads from it.

DisplayStateCache

Caches the fully-computed display state for a match. Avoids recomputing on every API request.
KeyTypeDescription
matchId (PK)StringFixture match ID
TTL: Yes — cache entries expire and are rebuilt on next request. Accessed by: DisplayStateCacheService (read/write) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get cached display stateBothGetItemPK=cacheKey (composite: DISPLAY#{matchId}#{teamId})GET /matches/:id/display, LiveScoreJobPer request / per tick
Set cached display stateBothPutItemPK=cacheKey with TTLDisplayStateService after computationAfter each state computation
The cache key is DISPLAY#{matchId}#{teamId}, allowing per-team display state caching. Client-side TTL validation is performed in addition to DynamoDB TTL — if ttl has passed, the GetItem returns null even before DynamoDB deletes the record.

cricket-fixture-cache

Caches raw fixture data from SportMonks to reduce API calls.
KeyTypeDescription
matchId (PK)StringFixture match ID
TTL: Yes — cached data expires based on match state (live matches have shorter TTLs). Accessed by: FixtureCacheService (read/write), SportMonksClient (via provider) GSIs:
GSI namePartition keySort keyPurpose
leagueId-startingAt-indexleagueIdstartingAtQuery cached fixtures by league
Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get cached fixtureBothGetItemPK=matchIdGET /matches/:id, LiveScoreJobPer request / per tick
Batch get cached fixturesBothBatchGetItemPK=matchId (up to 100, with retry)GET /matches, FixtureSyncJobPer request / per sync
Get fixtures by leagueBothQuery (GSI)leagueId-startingAt-index, leagueId = :lid with filter ttl > :nowGET /matches?league=, LiveScoreJobPer request / per tick
Get all cached fixturesSystemScanFull table scan (30-50 items)FixtureSyncJobPer sync cycle
Put single fixtureSystemPutItemPK=matchIdLiveScoreJobPer live match per tick
Batch put fixturesSystemBatchWriteItemPK=matchId (batches of 25, with retry)FixtureSyncJobPer sync cycle
Update enrichment onlySystemUpdateItemPK=matchId with ConditionExpression: attribute_exists(matchId)ActivityPushJobPer live match per tick
putBatch performs a read-before-write to preserve existing enrichment data: it BatchGets all items first, merges enrichment if the incoming entry has none, then BatchWrites. This prevents the sync job from overwriting enrichment data written by the activity push job.

match-processing-state

Tracks ball-by-ball processing progress for a match. Ensures the system can resume processing from the last known ball after restarts.
KeyTypeDescription
matchId (PK)StringFixture match ID
Key attributes: ballByBall (full ball data), lastProcessedBall (pointer to resume from) Accessed by: MatchProcessingService (read/write) Access patterns:
PatternTypeOperationKey conditionTriggered byFrequency
Get last processed ball IDSystemGetItem (on cache miss)PK=matchIdLiveScoreJob ball-by-ball processingPer live match per tick (usually in-memory)
Set last processed ball IDSystemPutItem (async write-through)PK=matchId with 7-day TTLLiveScoreJob after processing a ballPer new ball detected
Hydrate cache from DynamoDBSystemGetItem per match or ScanPK=matchId per active match, or full scanService startupOnce on boot
This service uses a hybrid storage pattern: an in-memory Map is the primary store for O(1) reads, with DynamoDB as a durable backup via async write-through. On startup, hydrateFromDynamo populates the in-memory cache from DynamoDB. The Scan path is only used when no active match list is available.

Access pattern summary

ServiceTables accessedMode
FixtureServicecricket-fixturesRead/Write
TeamServicecricket-teamsRead/Write
TeamSeasonServicecricket-team-seasonsRead/Write
LeagueServicecricket-leaguesRead/Write
DeviceServicecricket-devicesRead/Write
MatchFollowServicecricket-match-followsRead/Write
ActivitySubscriptionServicecricket-activity-subscriptionsRead/Write
PrematchNotificationServicecricket-prematch-notificationsRead/Write
LockServicecricket-locksRead/Write
StateHashServicecricket-state-hashesRead/Write
DisplayStateCacheServiceDisplayStateCacheRead/Write
FixtureCacheServicecricket-fixture-cacheRead/Write
MatchProcessingServicematch-processing-stateRead/Write
PushNotificationServicecricket-devices, cricket-match-followsRead
LiveActivityPushServicecricket-activity-subscriptionsRead
DisplayStateServicecricket-fixtures, cricket-state-hashesRead
FixtureEnrichmentProvidercricket-teams, cricket-leaguesRead