Back to AboutEngineering Deep Dive

Building Minute93: Engineering a Real-Time Football Platform

How I designed and built an event-driven distributed system to deliver live football data, from API polling to browser push in under 500ms.

15 min readBy Azmain Morshed
Chapter 1

Motivation

Every engineering portfolio needs a project that goes beyond CRUD. Something that deals with real-time data, concurrent consumers, cache invalidation, and the kind of infrastructure decisions that come up in production systems.

I wanted to build something I genuinely cared about. Not a todo app, not a Twitter clone, but a system that processes live events, updates multiple data stores in parallel, and pushes changes to users in real time. Football was the perfect domain.

“The best portfolio project is one where the requirements are genuinely complex, not artificially complicated.”
Chapter 2

Architecture Overview

Minute93 follows an event-driven architecture with a clear separation between data ingestion, processing, and delivery. Here's the high-level view:

API-Football (External)
  → Poller Worker (cron-based, dedup via Redis)
    → Kafka [match.events topic]
      ├─ CacheUpdater   → Redis (live scores)
      ├─ PostgresWriter  → Database (historical)
      ├─ StatsAggregator → Materialized Views
      └─ SsePublisher   → Redis Pub/Sub → SSE → Browser

Each Kafka consumer is independent. If one fails, the others continue processing. This gives us resilience without complexity. The poller writes to Kafka, and everything downstream is eventually consistent.

Chapter 3

The Event Pipeline

The heart of Minute93 is its event pipeline. A poller worker hits the API-Football endpoints on a configurable interval, deduplicates events using Redis Sets, and publishes new events to Kafka.

Deduplication

Redis SADD ensures each event is processed exactly once, even if the poller fetches overlapping data.

Fan-out

Kafka distributes each event to 4 consumer groups simultaneously. No bottleneck, no coupling.

Rate Limiting

The poller respects API-Football's rate limits with token bucket + Redis counters.

Backpressure

If consumers lag, Kafka retains events. Nothing is lost. Processing just catches up.

Chapter 4

Real-Time Delivery

Getting data from Kafka to the browser requires bridging two worlds: the backend event stream and the frontend. I chose Server-Sent Events (SSE) over WebSockets for several reasons:

  • Unidirectional: the server pushes, the client listens. Perfect for live scores.
  • Auto-reconnects: built into the EventSource API, no library needed.
  • No protocol upgrade: works over standard HTTP, friendly to proxies and load balancers.
  • Simpler to debug: it's just a long-lived HTTP response with text/event-stream content type.

The SsePublisher Kafka consumer receives events and publishes them to a Redis Pub/Sub channel. The NestJS SSE controller subscribes to that channel and streams events to connected browsers.

Chapter 5

Data Layer

The data layer uses PostgreSQL for durable storage and Redis for hot reads. Standings and top scorer rankings are computed as materialized views, refreshed concurrently after each batch of events.

Search is powered by PostgreSQL's pg_trgm extension, trigram-based fuzzy matching that tolerates misspellings without needing Elasticsearch. A GIN index on player and team names keeps it fast.

“You don't always need a separate search engine. PostgreSQL trigram search with a GIN index handles fuzzy matching surprisingly well for datasets under a million rows.”
Chapter 6

Lessons Learned

Building Minute93 taught me several things that don't show up in architecture diagrams:

Start with the data flow

Draw the path from source to browser before writing code. It reveals coupling you'd otherwise discover too late.

Idempotency is non-negotiable

In an event-driven system, every consumer must handle duplicate events gracefully. Design for at-least-once delivery from day one.

Observability before optimization

I added Prometheus metrics and structured logging early. When something broke in production, I could see exactly where and why.

External APIs are unreliable

API-Football has rate limits, occasional downtime, and inconsistent response shapes. Every integration point needs a fallback.

Chapter 7

What's Next

Minute93 is a living project. Here's what's on the roadmap:

  • Load testing with k6 during live Champions League matchdays
  • Adding match prediction models using historical data
  • Mobile-optimized PWA with push notifications
  • Grafana dashboards for real-time system health monitoring

The goal is to run this system live during the Champions League 2025-26 season and write about what happens when real traffic hits a distributed system I built from scratch.

Minute93

Explore the code

Minute93 is open source. Dive into the codebase and see every architectural decision in context.