Dots and Boxes Online
Realtime Multiplayer Platform for playing Dots and Boxes
Dots and Boxes
A real-time multiplayer platform for playing Dots and Boxes, featuring WebSocket-based gameplay, lobby system, and live chat. Built as a learning project to explore distributed systems architecture, event sourcing patterns, and production deployment strategies.
View on GitHub Try Live DemoOverview
I created this project to practice implementing a distributed system with real-time communication while having something fun and testable to share with friends and family. What started as a simple game implementation evolved into a sophisticated platform showcasing modern backend architecture and full-stack development practices.
Tech Stack
Backend
- Go with Echo framework
- PostgreSQL for persistence
- Redis as event bus for real-time updates
- WebSocket protocol for bidirectional communication
- Domain Driven Design
- Event Sourcing
Frontend
- React with TypeScript
- Real-time UI updates via WebSocket
- Tanstack Query
- Tanstack Router
- Zod Validation
- React Form
- Sonner
Infrastructure
- Docker containerization
- Caddy reverse proxy
- Proxmox VM
- GitHub Actions CI/CD
- Cloudflare Tunnel
Key Features
Real-Time Multiplayer
- WebSocket-based game state synchronization
- Lobby system
- In game chat functionality
- Play against other players or a basic bot opponent
Architecture Highlights
- Event Bus
- Proper separation of concerns using DDD principles
- Auth system
- Dependency Injection
- Event Sourcing
Development Journey
This project went through significant architectural evolution. I started with a simple service oriented architecture approach and refactored to a proper Domain-Driven Design architecture with an event bus, which provided valuable experience with:
- Implementing event-driven communication patterns
- Managing WebSocket connections and connection lifecycle
- Building responsive real-time UI with complex state management
The most challenging aspects were trying to figure out the boundaries between packages. For example I was struggling figuring out how to separate my WebSocket from being coupled with everything else in the system.
What I Learned
Docker Containerization with Docker Compose Deployment
Containerized the application using Docker with Docker Compose orchestration. This eliminated environment-specific issues and made the project immediately runnable on any machine, whether my local development setup or production infrastructure. Deployment became as simple as cloning the repo and running docker compose up.
CI/CD with GitHub Actions
Every push to main triggers an automated deployment pipeline. The workflow SSHes into my Proxmox homelab VM, pulls the latest code, and rebuilds the containers with docker compose up -d --build , with zero manual steps required.
The interesting challenge here was that my homelab isn’t publicly exposed. Rather than opening an SSH port to the internet, the pipeline connects through a Cloudflare Tunnel using cloudflared as an SSH proxy command. This keeps the server completely off the public internet while still allowing GitHub Actions’ runners to reach it securely using short-lived Cloudflare Access service tokens stored as repository secrets.
Importance of Separation of Concerns
I structured the codebase around single responsibility principles, ensuring each service and function served one clear purpose. This architectural decision proved invaluable during refactoring. Changes to individual components remained isolated, allowing me to evolve the system confidently without cascading side effects.
Event Sourcing for Game Replays
I reached for event sourcing specifically to make game replays easy to implement. Rather than storing just the current game state, every action is appended to an immutable event log , so replaying a game is as simple as reading the log back from the beginning. It worked exactly as I hoped, and the implementation was surprisingly straightforward once the event schema was in place. What I didn’t anticipate was the tradeoff that comes with immutability. When a guest account is upgraded to a registered user, the historical event log still references the old guest identity, because those events were written at a point in time and don’t retroactively update. It’s a small bug but it was a good reminder that you still have to think carefully about identity changes when your event log is immutable.
Future Enhancements
I plan on adding a smarter bot that would use the minimax algorithm with alpha-beta pruning rather than if conditionals.