DEV Community

Erik Novikov
Erik Novikov

Posted on

How I built dotapro.org

Intro

This post breaks down my journey building dotapro.org, an open-source Dota 2 analytics platform for professional games. I'll cover why I built it, the technical architecture behind it, and share some actionable tips that might benefit you as a dev.

Why I built it

Dota 2 is one of the few games I occasionally enjoy, and I love building useful software. When I couldn't easily find specific professional Dota 2 analytics, I realized other fans and pro players had the exact same problem. So, I decided to build a solution myself.

Features

  • Advanced filtering for match and series data, allowing highly optimized queries by league, team, player, and hero.
  • A sleek, modern UI focused on the match data that actually matters to players.
  • A real-time ETL (Extract, Transform, Load) pipeline to keep match data up to date.
  • Optimized, near-real-time pg_trgm word similarity searches for league, player, and team names.

Architecture Overview

When users visit the website, the React frontend fetches JSON data from the backend (api-lambda). Meanwhile, on a scheduled basis, a separate ETL service (scraper-lambda) fetches batch data from the OpenDota API, processes it, and inserts it into our database.

Architecture

The entire infrastructure runs on AWS, the currently leading cloud provider. So the project was a great opportunity to get some real world practice with it.

Storage

For the database, I used AWS RDS (Relational Database Service) with PostgreSQL. RDS handles much of the operational complexity for you. At one point, I temporarily ran an EC2 instance with an EBS volume just to experience what it actually means to operate a database manually (OS patching, vertical scaling, automated backups, etc.). While doing it manually saves a few bucks, I quickly learned that managed services are worth the cost for the time they save. I chose PostgreSQL specifically for its ability to handle the app's advanced filtering requirements.

Compute

The backend consists of two AWS Lambdas:

  1. Scraper Lambda: Runs on an AWS EventBridge schedule every 5 minutes, processing new match data and inserting it into the PostgreSQL database.
  2. API Lambda: A monolithic Lambda serving the API, handling request routing at the application level (via go-chi) rather than at the API Gateway level.

I opted for a monolithic API Lambda for two main reasons:

  • Simplicity and ease of deployment: All backend API functionality lives in one place. If I need to make a change, I just update a single Lambda's source code.
  • Performance: If a user hits the /matches endpoint, any subsequent request will benefit from a warm start regardless of its path. This leverages the Lambda execution context, avoiding the need to re-initialize database connection pools, which would otherwise cause latency.

This "Lambda-as-a-Server" approach is common, but you must always weigh the tradeoffs. For instance, it makes sense to split a monolith into multiple single-purpose Lambdas if:

  • You're in a large organization where separate teams work on separate modules. A massive shared codebase quickly becomes a nightmare for concurrent deployments and bug fixes.
  • You have a specific piece of "hot" functionality (used 10x more than other endpoints) or a module that absolutely cannot tolerate cold starts (like a payments webhook). In that case, you'd create a dedicated Lambda with provisioned concurrency.

Frontend

The frontend is a React app built with TypeScript for type safety, Tailwind CSS for styling, Vite for bundling, and TanStack Router/Query for state and routing. It is deployed on an Amazon S3 bucket behind an AWS CloudFront distribution. This is the industry standard for hosting static websites on AWS—it offers maximum performance (users fetch content from their nearest edge location) and excellent cost-effectiveness.

Deployment and CI/CD

Pushing code to the main branch triggers a GitHub Actions workflow that selectively deploys only the directories that have changed. To allow Github Actions to call AWS services on your behalf, you have to set up OIDC auth.

  • To redeploy the UI: The action installs Node.js and dependencies, builds the Vite-optimized HTML/CSS/JS bundle, syncs it to S3, and creates a CloudFront cache invalidation so changes take effect immediately.
  • To redeploy the lambdas: The action installs Go, compiles the correspodning source code into a binary, and updates the Lambda via the aws lambda update-function-code API.

Actionable Tips

  • KISS (Keep It Simple, Stupid): It's a cliché, but coupled with avoiding premature optimization, it's a principle devs (myself included) constantly forget. We tend to overengineer, adding complexity just for the sake of it, completely overlooking how time-consuming that complexity becomes. For example, I initially built the scraper with a decoupled architecture: one Lambda scraped OpenDota, pushed data to an SQS Queue, and an ingestor-lambda polled the queue to update the DB. I eventually realized a single Lambda could handle the entire workload—making it simpler, cheaper, and faster. Only introduce complexity when the app really requires it.
  • Action drives motivation (not the other way around): It's natural to lose a bit of passion when you hit a roadblock or work on a tedious feature. That is totally OK. Embrace the temporary boredom, push your way through the roadblock, and the motivation will return. And if tech is stressing you out, watch this Jeff Bezos short.
  • Build something you personally find useful: Software development is not only a career, but a lifestyle. So you should do lots of it. Naturally, building things you actually want to use will give you the most satisfaction. Whether it was this Dota project, or a this custom Linux voice dictation tool I built, solving my own problems with software has been incredibly rewarding.

Conclusion

I hope you took something away from this post. Feel free to drop a comment if you agree / disagree with anything, or have a story of your own to share. Happy coding!

Top comments (0)