I started Stream Wars as a demo project to explain how Kafka works using something more fun than log lines.
Instead of having a producer script send fake JSON into a topic, every tap in the browser becomes a real event that flows through Kafka, Redis, and back to the UI in real time.
Stream Wars - a simple game to explain Kafka
Why I Built It
The goal was to show three things:
- How frontend actions (taps) become Kafka events
- How topics are designed around use-cases, not tables
- How you can see those events both in the UI and in Kafka tooling
So I ended up with three main topics:
game-taps– every tap from a player (userId, team, timestamp, sessionId)game-updates– processed game state (team scores, total taps, leaderboard)user-metadata– connection metadata (browser, IP, language) for analytics
How a Tap Becomes an Event
The flow is deliberately simple:
- The browser sends a POST to
/api/tapwhen you tap. - The Next.js API publishes a message to the
game-tapstopic. - A Kafka consumer reads
game-taps, updates Redis (scores, totals, user taps). - The consumer publishes a summarized state to
game-updates. - A WebSocket server pushes that update to every connected client.
From Kafka’s point of view, it’s just:
- One producer writing tap events
- One consumer updating state
- One real-time fan-out via WebSockets
The nice part is that you can open the game in your browser, start tapping, and at the same time run:
docker exec -it kafka ./opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic game-taps \
--from-beginning
You literally see your taps as Kafka events in the terminal.
The Architecture
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Browser │ │ Next.js │ │ Kafka │
│ (Taps) │───►│ Producer │───►│ game-taps │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ Consumer │
│ (Updates │
│ Redis) │
└─────────────┘
|
▼
┌─────────────┐
│ WebSocket │
│ Broadcast │
└─────────────┘
Try It Yourself
Start the Game
# Clone and start
git clone https://github.com/joel-hanson/stream-wars
cd stream-wars
docker-compose up
Then open http://localhost:3000 and start tapping!
Watch Events in Real-Time
In another terminal, watch the tap events flow:
# Watch game-taps topic
docker exec -it kafka ./opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic game-taps \
--from-beginning \
--property print.key=true \
--property print.timestamp=true
You’ll see events like:
{"userId":"abc123","username":"Player1","team":"blue","timestamp":1703001234567,"sessionId":"xyz"}
{"userId":"def456","username":"Player2","team":"red","timestamp":1703001234568,"sessionId":"abc"}
Inspect Topics
# List all topics
docker exec -it kafka ./opt/kafka/bin/kafka-topics.sh \
--bootstrap-server localhost:9092 \
--list
# Check topic details
docker exec -it kafka ./opt/kafka/bin/kafka-topics.sh \
--bootstrap-server localhost:9092 \
--describe \
--topic game-taps
Key Learnings
1. Events as First-Class Citizens
Each tap is an event with:
- A unique ID
- A timestamp
- User context (userId, team, sessionId)
This makes it easy to replay, debug, and analyze later.
2. Single Source of Truth
Redis is the authoritative state store. The consumer:
- Reads tap events from Kafka
- Updates Redis atomically
- Broadcasts state via WebSocket
This eliminates race conditions and ensures accurate counts.
3. Decoupling Frontend and Backend
The frontend doesn’t know about Kafka. It just:
- Sends HTTP requests
- Receives WebSocket updates
Kafka handles the event streaming behind the scenes.
Using It as a Demo
This tiny game turned into a good way to:
- Explain event-driven thinking (state derived from a stream of taps)
- Talk about idempotency and counting (why Redis is the source of truth)
- Show how Kafka sits between a frontend and a backend without anyone noticing
- Demonstrate real-time updates across multiple clients
The Tech Stack
- Next.js 16 – Frontend and API routes
- Kafka (KRaft mode) – Event streaming (no Zookeeper needed!)
- Redis – Game state and leaderboard
- WebSockets – Real-time updates
- TypeScript – Type safety throughout
Conclusion
Stream Wars shows that you don’t need a complex system to demonstrate Kafka’s power. Sometimes the best way to explain event streaming is to make it tangible – where every action you take becomes an event you can see.
If you want to try it or adapt it for your own demos, the code is here:
GitHub Repository
For more Kafka tips and real-time application insights, follow the blog series