<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Yash on Medium]]></title>
        <description><![CDATA[Stories by Yash on Medium]]></description>
        <link>https://medium.com/@yashh21?source=rss-86180f878763------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*BJuyM7aU9W1X5UINc1MwcA.jpeg</url>
            <title>Stories by Yash on Medium</title>
            <link>https://medium.com/@yashh21?source=rss-86180f878763------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 04 Nov 2026 15:00:45 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@yashh21/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How I built a BitTorrent client called GoTorrent for my college final-year project.]]></title>
            <link>https://yashh21.medium.com/how-i-built-a-bittorrent-client-called-gotorrent-for-my-college-final-year-project-b059e6bdceeb?source=rss-86180f878763------2</link>
            <guid isPermaLink="false">https://medium.com/p/b059e6bdceeb</guid>
            <category><![CDATA[bittorrent]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[golang]]></category>
            <dc:creator><![CDATA[Yash]]></dc:creator>
            <pubDate>Thu, 14 May 2026 12:48:26 GMT</pubDate>
            <atom:updated>2026-05-14T12:51:19.403Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/923/1*0v0BiA0YbTwwG2SBNRNs3Q.png" /></figure><p>This blog is divided into 7 parts:</p><ol><li>Why This Project</li><li>Understanding BitTorrent</li><li>GoTorrent Architecture</li><li>System Workflow: How GoTorrent Operates</li><li>The P2P Engine (Under the Hood)</li><li>Key Features and Big Challenges</li><li>Conclusion and Learnings</li></ol><h3>1. Why This Project</h3><p>Before diving into the technical details, here is why I chose this project.</p><p>When I was around 12–13 years old, I used to download games and movies using applications like uTorrent. Those early experiences made me curious about how files could be shared among many users without relying on a single central server.</p><p>That curiosity eventually led me to build GoTorrent as my final-year project, both to understand the BitTorrent protocol deeply and to challenge myself with a real-world networking system.</p><h3>2. Understanding BitTorrent</h3><p>Before explaining GoTorrent, it helps to understand how BitTorrent works.</p><p>BitTorrent is a peer-to-peer (P2P) protocol where each user (peer) can both download and upload. Instead of downloading an entire file from one server, the file is split into pieces and shared across peers.</p><h4>2.1 Torrent File and Tracker</h4><p>A .torrent file contains metadata such as:</p><ul><li>File name and size</li><li>Piece length and SHA-1 hashes</li><li>Tracker URLs</li></ul><p>When a client loads this file, it contacts a tracker to get a list of peers sharing that torrent.</p><h4>2.2 Peer-to-Peer Communication</h4><p>After peers are discovered, they connect directly over TCP and exchange protocol messages such as:</p><ul><li>handshake for connection setup</li><li>interested / not interested</li><li>choke / unchoke</li><li>request, piece, have</li></ul><h4>2.3 Piece Verification</h4><p>Each piece is verified using SHA-1 hashes from torrent metadata. If a hash check fails, that piece is downloaded again.</p><h4>2.4 Seeding and Swarming</h4><p>Once a peer completes the file, it becomes a seeder and uploads pieces to other peers. This cooperative model is what makes BitTorrent scalable.</p><p>In short, BitTorrent is a distributed exchange of pieces among peers, not a one-way client-server download.</p><h3>3. GoTorrent Architecture</h3><p>GoTorrent follows a daemon-based architecture built in Go.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*yV6jf4vmq2tv75C8-hfo2w.png" /></figure><h4>3.1 Daemon (gotorrentd): The Engine</h4><p>The daemon handles core responsibilities:</p><ul><li>pkg/daemon: orchestration, lifecycle control, and coordination</li><li>pkg/torrent: .torrent parsing and tracker communication (HTTP/UDP)</li><li>DHT integration in daemon for additional peer discovery</li><li>pkg/p2p, pkg/client, pkg/message: peer protocol engine and piece transfer</li><li>pkg/daemon/store.go + SQLite: persistence of state, progress, and bitfield</li></ul><p>Note: Magnet link support is planned, but current implementation primarily handles .torrent input.</p><h4>3.2 Client Layer</h4><p>Users interact with the daemon through:</p><ul><li>CLI client via RPC</li><li>REST endpoints for programmatic control</li></ul><h4>3.3 Why Go</h4><p>Go was a strong fit because of:</p><ul><li>Goroutines and channels for concurrency</li><li>Reliable networking primitives for TCP/UDP/HTTP</li><li>Good runtime performance for high-throughput transfers</li></ul><p>This architecture keeps core download logic isolated and long-running, while control surfaces remain lightweight.</p><h3>4. System Workflow: How GoTorrent Operates</h3><h4>4.1 Parsing The .torrent file via bencode-go parser</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WknaLrAvfSpcaIhGYv9YTQ.png" /><figcaption>Bencode parser parses the torrent file into json format with all the information which exists in the .torrent file</figcaption></figure><pre>type bencodeInfo struct {<br> Pieces      string `bencode:&quot;pieces&quot;`<br> PieceLength int    `bencode:&quot;piece length&quot;`<br> Length      int    `bencode:&quot;length&quot;`<br> Name        string `bencode:&quot;name&quot;`<br>}</pre><pre>type bencodeTorrent struct {<br> Announce     string      `bencode:&quot;announce&quot;`<br> AnnounceList [][]string  `bencode:&quot;announce-list&quot;`<br> Info         bencodeInfo `bencode:&quot;info&quot;`<br>}</pre><pre>func (bto *bencodeTorrent) toTorrentFile() (TorrentFile, error) {}</pre><p>This Function parses the data and splits the hashes and fill up the trackers array with the trackers.</p><h4>4.2 Contacting the Tracker and Peer Discovery</h4><p>After parsing, we get a TorrentFile model:</p><pre>type TorrentFile struct {<br>    Announce     string<br>    InfoHash     [20]byte<br>    PieceHashes  [][20]byte<br>    AnnounceList []string<br>    PieceLength  int<br>    Length       int<br>    Name         string<br>}</pre><p>The tracker request starts in pkg/torrent/tracker.go. GoTorrent builds a standard tracker URL with query params like:</p><ul><li>info_hash</li><li>peer_id</li><li>port</li><li>uploaded, downloaded, left</li><li>compact=1</li></ul><p>Core builder:</p><pre>func buildTrackerURL(tf *TorrentFile, peerID [20]byte, port uint16) (string, error)</pre><p>The main entry point is:</p><pre>func RequestPeers(tf *TorrentFile, peerID [20]byte) ([]peers.Peer, error)</pre><p>This function checks tracker scheme and routes to:</p><ul><li>HTTP/HTTPS tracker flow: requestPeersHTTP(...)</li><li>UDP tracker flow: RequestPeersUDPWithFallback(...)</li></ul><h4>HTTP tracker response model</h4><pre>type bencodeTrackerResp struct {<br>    Interval       int    `bencode:&quot;interval&quot;`<br>    Peers          string `bencode:&quot;peers&quot;`<br>    FailureReason  string `bencode:&quot;failure reason,omitempty&quot;`<br>    WarningMessage string `bencode:&quot;warning message,omitempty&quot;`<br>    Complete       int    `bencode:&quot;complete,omitempty&quot;`<br>    Incomplete     int    `bencode:&quot;incomplete,omitempty&quot;`<br>}</pre><p>The response is decoded and compact peers are converted using:</p><pre>func UnmarshalPeers(peersBin []byte) ([]Peer, error)</pre><p>where each peer is parsed from 6 bytes (4 for IP + 2 for port).</p><h4>UDP tracker flow and resilience</h4><p>pkg/torrent/udp.go implements the UDP tracker protocol in two phases:</p><ol><li>Connect (actionConnect)</li><li>Announce (actionAnnounce)</li></ol><p>Key functions:</p><pre>func requestPeersUDP(u *url.URL, tf *TorrentFile, peerID [20]byte) ([]peers.Peer, error)<br>func buildConnectRequest(transactionID uint32) []byte<br>func parseConnectResponse(data []byte, expectedTransactionID uint32) (uint64, error)<br>func buildAnnounceRequest(connectionID uint64, transactionID uint32, tf *TorrentFile, peerID [20]byte, port uint16) []byte<br>func parseAnnounceResponse(data []byte, expectedTransactionID uint32) ([]peers.Peer, error)</pre><p>To improve reliability, sendAndReceive(...) uses retry + exponential backoff, and RequestPeersUDPWithFallback(...) tries known fallback UDP trackers if the primary one fails.</p><h4>4.3 Hybrid Peer Discovery Inside the Daemon</h4><p>Tracker contact alone is not enough for all torrents, so GoTorrent adds DHT discovery in the daemon (pkg/daemon/server.go).</p><p>In Server.AddTorrent(...), peer discovery happens in this order:</p><ol><li>Iterate tracker URLs from AnnounceList</li><li>Query trackers in order via torrent.RequestPeers(...) until one returns peers</li><li>If DHT is enabled, query DHT using s.dht.Announce(tf.InfoHash, 0, true)</li><li>Merge and deduplicate peers using IP:Port keys</li></ol><p>This gives a hybrid discovery strategy:</p><ul><li>Trackers provide fast bootstrap peers</li><li>DHT improves availability when trackers are down or limited</li></ul><h4>4.4 End-to-End Workflow (Execution Pipeline)</h4><p>Once peers are found, the daemon creates the P2P job:</p><pre>torrentJob := &amp;p2p.Torrent{<br>    Peers:        initialPeers,<br>    PeerID:       ourPeerID,<br>    InfoHash:     tf.InfoHash,<br>    PieceHashes:  tf.PieceHashes,<br>    PieceLength:  tf.PieceLength,<br>    Length:       tf.Length,<br>    Name:         tf.Name,<br>    Announce:     tf.Announce,<br>    DownloadPath: s.downloadPath,<br>}</pre><p>Then the workflow is:</p><ol><li>Persist torrent metadata in SQLite (store.AddTorrent)</li><li>Add job to in-memory map (s.torrents)</li><li>Start async download worker (go torrentJob.DownloadTheFile(s.store))</li><li>Periodically update progress (store.UpdateTorrentProgress)</li><li>Serve status through RPC + REST APIs</li></ol><p>So the full data path is:</p><p>.torrent -&gt; metadata parse -&gt; peer discovery (Tracker + DHT) -&gt; worker swarm -&gt; piece validation -&gt; disk write -&gt; DB update -&gt; status APIs</p><h3>5. The P2P Engine (Under the Hood)</h3><p>This is the core download logic in pkg/p2p/p2p.go and related protocol packages.</p><h4>5.1 Torrent Runtime Model</h4><pre>type Torrent struct {<br>    Peers           []peers.Peer<br>    PeerID          [20]byte<br>    InfoHash        [20]byte<br>    PieceHashes     [][20]byte<br>    PieceLength     int<br>    Length          int<br>    Name            string<br>    Announce        string<br>    BytesDownloaded int64<br>    Status          string<br>    ActivePeers     int<br>    State           uint8<br>    OurBitfield     bitfield.Bitfield<br>    DownloadPath    string<br>}</pre><p>This struct tracks both static metadata (hashes, piece size, total length) and mutable runtime state (status, speed, active peers, bitfield).</p><h4>5.2 Peer Connection, Handshake, and Bitfield</h4><p>Each worker connects through client.New(...):</p><ul><li>TCP connect to peer IP:Port</li><li>Perform BitTorrent handshake</li><li>Receive initial bitfield</li></ul><p>Handshake package (pkg/handshake/handshake.go) defines:</p><pre>type Handshake struct {<br>    Pstr     string<br>    InfoHash [20]byte<br>    PeerID   [20]byte<br>}</pre><p>completeHandshake(...) verifies returned InfoHash matches the target torrent, preventing cross-torrent contamination.</p><h4>5.3 Piece Scheduling and Parallel Download</h4><p>The engine uses a producer-consumer model:</p><ul><li>workQueue contains pending pieces (pieceWork)</li><li>Each peer goroutine runs startDownloadWorker(...)</li><li>Successful pieces are sent through results</li></ul><p>Per-piece download uses pipelining with backlog:</p><pre>const MaxBlockSize = 16384<br>const MaxBacklog = 5</pre><p>attemptDownloadPiece(...) keeps requesting blocks while unchoked and reads protocol messages (Unchoke, Have, Piece, etc.) via readMessage().</p><h4>5.4 Integrity and Safe Write Path</h4><p>Before writing piece data, GoTorrent validates SHA-1:</p><pre>func checkIntegrity(pw *pieceWork, buf []byte) error</pre><p>If hash check fails, piece is re-queued. If it passes, piece is written at exact file offset with WriteAt(...), and local bitfield is updated.</p><h4>5.5 Pause, Resume, Stop, and Speed Tracking</h4><p>Torrent control uses a state channel:</p><pre>func (t *Torrent) Pause()<br>func (t *Torrent) Resume()<br>func (t *Torrent) Stop()</pre><p>Download speed is calculated every 2 seconds from byte deltas. Progress is persisted periodically so restarts can continue from saved bitfield state.</p><h3>6. Key Features and Big Challenges</h3><h4>6.1 Key Features Implemented (Go Backend)</h4><ol><li>Hybrid peer discovery</li><li>HTTP and UDP tracker support</li><li>Multi-peer concurrent piece download using goroutines</li><li>Piece-level SHA-1 integrity validation</li><li>Persistent progress with SQLite (including bitfield)</li><li>Pause/resume/remove lifecycle management</li><li>Daemon architecture with RPC and REST interfaces</li><li>Real-time status broadcasting pipeline</li></ol><h4>6.2 Big Technical Challenges</h4><ol><li><strong>Unreliable tracker ecosystem</strong> Many trackers fail, timeout, or return empty peer lists. I solved this using multi-tracker attempts, UDP retries, and fallback tracker logic.</li><li><strong>Peer instability and choking behavior</strong> Not all peers cooperate. Some choke, disconnect, or have partial data. I handled this using per-worker timeouts, piece re-queueing, and concurrent workers.</li><li><strong>Correct piece boundary math</strong> Last piece is often smaller than normal piece length. I added calculateBoundsForPiece(...) and calculatePieceSize(...) to avoid overflow and corruption.</li><li><strong>Data integrity under concurrency</strong> Concurrent downloads can cause races if state updates are unsafe. I used mutexes for shared state (BytesDownloaded, bitfield, status) and only mark piece complete after successful hash + disk write.</li><li><strong>Crash/restart resilience</strong> Download clients should not lose progress after restart. I persisted status + bitfield in SQLite and load managed torrents from DB during daemon startup.</li><li><strong>Lifecycle transitions are tricky</strong> Pausing, resuming, stopping, and restarting workers can create invalid states. I made explicit state transitions (StatePaused, StateDownloading, StateSeeding, etc.) and guarded resume behavior based on current status.</li></ol><h3>7. Conclusion and Learnings</h3><p>Building GoTorrent as my final year project taught me far more than just “how torrents work”. It gave me hands-on experience with distributed systems, binary protocols, concurrency, fault tolerance, and long-running backend service design in Go.</p><p>The biggest learning was this: A BitTorrent client is not just about downloading files. It is a coordination problem across unreliable networks, inconsistent peers, and asynchronous state updates.</p><p>From parsing bencoded metadata to tracker communication, DHT lookup, handshake negotiation, piece scheduling, integrity verification, and persistence, every stage has failure modes that must be handled explicitly.</p><p>Go’s goroutines, channels, and standard library made it practical to build this as a real production-style system, not just a classroom prototype.</p><p>You can explore the complete source code and implementation details here: <a href="https://github.com/Yashh56/GoTorrent">https://github.com/Yashh56/GoTorrent</a></p><p>If you found the project interesting or helpful, feel free to leave a star on the repository ⭐</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b059e6bdceeb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Design a Simple Real-Time Matchmaking Service: Architecture & Implementation]]></title>
            <link>https://yashh21.medium.com/designing-a-simple-real-time-matchmaking-service-architecture-implementation-96e10f095ce1?source=rss-86180f878763------2</link>
            <guid isPermaLink="false">https://medium.com/p/96e10f095ce1</guid>
            <category><![CDATA[code]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[backend]]></category>
            <category><![CDATA[system-design-project]]></category>
            <category><![CDATA[golang]]></category>
            <dc:creator><![CDATA[Yash]]></dc:creator>
            <pubDate>Sat, 05 Jul 2025 15:40:37 GMT</pubDate>
            <atom:updated>2025-07-05T17:49:38.223Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/585/0*m49n27KBj9eVN5in.jpeg" /></figure><p><em>“In multiplayer online games, matchmaking is the core system that pairs players together in real-time battles, ranked matches, or casual games. Without an efficient matchmaking engine, players might experience long wait times, unfair matchups, and poor game experiences.”</em></p><p><em>This blog walks through the architecture and implementation of a simple but scalable matchmaking engine, covering its major services and logic.</em></p><p><strong>What This Blog Will Teach You</strong></p><ul><li>Matchmaking logic based on <strong>MMR</strong>, <strong>ping</strong>, and <strong>region.</strong></li><li>Real-time <strong>WebSocket notifications</strong> to clients when a match is found.</li><li>Using <strong>Redis Queue</strong> for managing players in matchmaking.</li><li>Implementing the <strong>Pub/Sub pattern</strong> for scalable async messaging.</li><li>Clean and <strong>modular architecture</strong> using internal/ and pkg/ folders.</li></ul><p><strong>What This Blog Will Not Teach You</strong></p><ul><li>The <strong>Game Orchestrator Service</strong> that dynamically creates Kubernetes pods.</li><li>Generating the <strong>WebSocket URL</strong> for players to connect to the game pod.</li><li>Automatic <strong>cleanup of game pods</strong> after a timeout (e.g., 5 minutes).</li><li>Deploying the full system on Kubernetes with auto-scaling.</li><li>Advanced matchmaking techniques like ML-based MMR adjustment.</li></ul><blockquote>These topics are more advanced and would make the blog too lengthy and complex for a single article. Since the complete source code is available on <a href="https://github.com/Yashh56/matchmakingEngine">GitHub</a>, you can explore the orchestrator and deployment details directly from there, or I’ll cover them in a follow-up blog.</blockquote><h3>Prerequisites</h3><p>Before we dive into building the matchmaking engine, make sure you’re familiar with the following:</p><ul><li><strong>Go:</strong> A working knowledge of Go (Golang) and how to structure a basic Go project.</li><li><strong>Redis:</strong> Understanding of Redis, as we’ll use it for queuing players and publishing match events.</li><li><strong>Kubernetes (Basic):</strong> Some basic understanding of Kubernetes concepts (although this blog won’t cover Kubernetes in detail, it’s part of the larger system architecture).</li><li><strong>HTTP &amp; WebSockets:</strong> Familiarity with how HTTP APIs and WebSocket connections work.</li></ul><h4><strong>Low-Level System Design :</strong></h4><p>Before Jumping into the code. Let’s look at how our system’s components interact.</p><h3><strong>Core Components</strong></h3><h4><strong>1. Player Service</strong> (🟧 Player Related):</h4><ul><li>Manages player data and matchmaking requests.</li><li>Places matchmaking requests into a <strong>Redis Queue</strong> for asynchronous processing.</li><li>Acts as the primary entry point for the matchmaking process.</li></ul><h4>2. Redis Queue</h4><ul><li><strong>Queue system</strong> that temporarily stores matchmaking requests.</li><li>Enables asynchronous decoupling between the Player Service and the Matchmaking Server.</li></ul><h4>3. Matchmaking Server (🟪 Core-Matchmaking Logic)</h4><ul><li>Consumes matchmaking requests from the Redis Queue.</li><li>Implements the <strong>core matchmaking algorithm</strong> (e.g., MMR-based pairing, latency checks).</li><li>Publishes matchmaking results to a <strong>Pub/Sub system</strong>.</li></ul><h4>4. Pub/Sub System</h4><ul><li>Decouples communication between the Matchmaking Server, Game Orchestrator, and Notification Services.</li><li>Facilitates <strong>asynchronous event distribution</strong>, such as when a match is found or a game server is created.</li></ul><h4>5. Notification Services (🟦 Notification/WebSockets)</h4><ul><li>Listens to Pub/Sub for real-time game/match events.</li><li>Sends real-time updates to connected clients via <strong>WebSockets</strong>, such as match found, game starting, or server errors.</li></ul><p>⚠️ <strong>Additional Components (Out of Scope for This Blog):</strong></p><h4>6. Game Orchestrator Service (🟫 Infrastructure Controller)</h4><ul><li>Listens to matchmaking events via Pub/Sub.</li><li>Responsible for <strong>creating or managing game server instances</strong>, usually on-demand.</li><li>Likely interacts with <strong>Kubernetes (K8s)</strong> for managing the game server lifecycle.</li></ul><h4>6. Kubernetes API Server (K8s Control Plane)</h4><ul><li>Exposes the Kubernetes API for managing infrastructure.</li><li>Allows the Game Orchestrator to spin up or terminate game server pods on-demand.</li></ul><figure><img alt="An Architecture Diagram of LLD Game Matchmaking Engine" src="https://cdn-images-1.medium.com/max/1024/1*w6-yLixyRroDdIKg2hMjjg.png" /></figure><blockquote>Important Note :</blockquote><blockquote>The Game Orchestrator and Kubernetes integration are part of the larger system architecture but are outside the scope of this blog. This article focuses on the matchmaking engine itself. These infrastructure components will be covered in a future blog post.</blockquote><h3>Setup</h3><p>Since we are going to build this matchmaking engine in <strong>Go</strong>, we assume you have some experience with Go and have it installed and configured.</p><p>We’ll also install a few essential Go packages using go get:</p><p>go get github.com/gorilla/mux</p><p><strong><em>Gorilla/mux</em></strong> is a popular router for writing HTTP handlers in Go.</p><p>go get github.com/gin-gonic/gin</p><p><strong><em>Gin</em></strong><em> </em>is a fast and lightweight HTTP web framework for Go. It features a clean API and much better performance compared to older frameworks like Martini.</p><p>go get github.com/joho/godotenv</p><p><strong><em>godotenv</em></strong><em> </em>is used for loading environment variables from .env files.</p><p>go get github.com/redis/go-redis/v9</p><p><strong><em>go-redis</em></strong> is the official Redis client library for Go, used to interact with Redis.</p><h4>Initializing the Redis Client</h4><p>We’ll create a utility package to initialize and access the Redis client throughout the project.</p><pre>//utils/redis.go<br><br>package utils<br><br>import (<br> &quot;fmt&quot;<br><br> &quot;github.com/redis/go-redis/v9&quot;<br>)<br><br>var client *redis.Client<br><br>func SetClient(redisClient *redis.Client) {<br> fmt.Println(&quot;Setting Redis client&quot;)<br><br> client = redisClient<br> fmt.Println(&quot;Redis client has been initialized&quot;)<br>}<br><br>func GetRedisClient() *redis.Client {<br> if client == nil {<br>  fmt.Println(&quot;WARNING: Redis client is nil&quot;)<br> }<br> return client<br>}</pre><p>This utility allows us to initialize the Redis connection once and access it from anywhere in the codebase.</p><h3><strong>Player Service</strong></h3><p>Let’s start by creating the <strong><em>Player Service</em></strong>, which is responsible for <strong>handling player matchmaking requests</strong>.</p><p>This service exposes a <strong>POST API endpoint</strong> where players submit their:</p><ul><li><strong>MMR (Matchmaking Rating)</strong></li><li><strong>Region</strong></li><li><strong>Ping</strong></li><li>and other game-related preferences.</li></ul><p>Once the player submits this data, the service places the matchmaking request into a <strong>Redis queue</strong>. This data will later be consumed by the <strong>Matchmaking Service</strong>, which uses it to group players into fair matches based on similar skill levels and network conditions.</p><p><strong>Data Model</strong></p><p>Let’s define a s<strong><em>truct</em></strong> called <em>Player</em> that contains various fields. Don’t worry, we’ll explain all of these in a minute.</p><pre>// internal/player/player.go<br><br>package player<br><br>type Player struct {<br> Player_id string `json:&quot;player_id&quot;`<br> MMR       int    `json:&quot;mmr&quot;`<br> Region    string `json:&quot;region&quot;`<br> Ping      int    `json:&quot;ping&quot;`<br> JoinedAt  int64  `json:&quot;joined_at&quot;`<br>}</pre><p>Player_id: A <strong>unique identifier</strong> for each player. This helps track them across sessions.</p><p>MMR : The <strong>Matchmaking Rating (MMR)</strong> that represents the player’s skill level. The matchmaking system uses this to form balanced teams.</p><p>Region: The <strong>geographic region</strong> where the player is located (e.g., Asia, Europe). Players from the same region are grouped to minimize latency.</p><p>Ping: The player’s <strong>current network latency</strong> in milliseconds. This is used to ensure a fair gameplay experience.</p><p>Joined_at : The <strong>timestamp</strong> when the player joined the matchmaking queue. This can be used for a timeout or prioritizing players waiting longer.</p><p><strong>Imports</strong></p><p>Here are the imports we’ll need, along with our package declaration. Let’s write these to handler.go</p><pre>// internal/player/handler.go <br>package player<br><br>import (<br> &quot;context&quot;<br> &quot;encoding/json&quot;<br> &quot;fmt&quot;<br> &quot;net/http&quot;<br><br> &quot;github.com/Yashh56/matchmakingEngine/utils&quot;<br> &quot;github.com/gin-gonic/gin&quot;<br> &quot;github.com/redis/go-redis/v9&quot;<br>)</pre><p>Now, Let’s Write the Join_queue function which handles the Player&#39;s Input and Inserts the data in the Redis Queue based on their region.</p><pre>var ct = context.Background()<br><br>func Join_queue(ctx *gin.Context) {<br> var player Player<br><br> // Bind JSON input<br> if err := ctx.ShouldBindJSON(&amp;player); err != nil {<br>  ctx.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})<br>  return<br> }<br><br> // Get Redis client<br> redisClient := utils.GetRedisClient()<br> if redisClient == nil {<br>  ctx.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Redis not initialized&quot;})<br>  return<br> }<br><br> // Serialize player struct to JSON string<br> playerBytes, err := json.Marshal(player)<br> if err != nil {<br>  ctx.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to serialize player&quot;})<br>  return<br> }<br> playerString := string(playerBytes)<br><br> // 1. Save player in Redis as key-value pair for future retrieval<br> err = redisClient.Set(ct, player.Player_id, playerString, 0).Err()<br> if err != nil {<br>  ctx.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to store player in Redis&quot;})<br>  return<br> }<br><br> // 2. Add player to matchmaking queue (sorted set)<br> queueKey := fmt.Sprintf(&quot;queue:%s:%s&quot;, &quot;solo&quot;, player.Region)<br><br> err = redisClient.ZAdd(ct, queueKey, redis.Z{<br>  Score:  float64(player.JoinedAt),<br>  Member: playerString,<br> }).Err()<br><br> if err != nil {<br>  ctx.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to add to queue&quot;})<br>  return<br> }<br><br> // 3. Retrieve stored player (optional check)<br> val, err := redisClient.Get(ct, player.Player_id).Result()<br> if err != nil {<br>  ctx.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to retrieve player from Redis&quot;})<br>  return<br> }<br><br> // Deserialize JSON back to struct<br> var retrievedPlayer Player<br> err = json.Unmarshal([]byte(val), &amp;retrievedPlayer)<br> if err != nil {<br>  ctx.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to decode player from Redis&quot;})<br>  return<br> }<br><br> // Success response<br> ctx.JSON(http.StatusOK, gin.H{<br>  &quot;status&quot;: &quot;Waiting in Queue&quot;,<br>  &quot;player&quot;: retrievedPlayer,<br> })<br>}<br></pre><h3>Matchmaking Service</h3><p>Now, let’s move on to the <strong>Matchmaking Service</strong>, which is responsible for pairing players into matches.</p><p>This service <strong>consumes player data from the Redis queue</strong> and applies matchmaking logic based on factors like <strong>MMR</strong> (Matchmaking Rating) and <strong>region</strong>.<br> Its goal is to group players into fair and balanced matches while keeping the matchmaking process fast and scalable.</p><p><strong>Imports</strong></p><pre>import (<br> &quot;context&quot;<br> &quot;encoding/json&quot;<br> &quot;fmt&quot;<br> &quot;math&quot;<br> &quot;time&quot;<br><br> &quot;github.com/Yashh56/matchmakingEngine/internal/player&quot;<br> &quot;github.com/google/uuid&quot;<br> &quot;github.com/redis/go-redis/v9&quot;<br>)</pre><p><strong>Data Model</strong></p><p>Let’s define a s<strong><em>truct</em></strong> called <em>Match</em> that contains various fields. Don’t worry, we’ll explain all of these in a minute.</p><pre>// internal/matchmaking/elo.go<br>type Match struct {<br> Id      string          `json:&quot;id&quot;`<br> Players []player.Player `json:&quot;players&quot;`<br> Region  string          `json:&quot;region&quot;`<br>}</pre><p>Id : A <strong><em>unique identifier A</em></strong>ssigned to each match (match Id).</p><p>Players : An Array of Player who has a Match based on their Data.</p><p>Region : The region in which the match will take place (used to reduce latency).</p><h4>Matchmaking Logic</h4><blockquote>Matching Players: Can They Be Matched?</blockquote><p>Before we run the matchmaking worker, we need a function to check whether two players can be fairly matched. This check considers their <strong>region</strong>, <strong>MMR gap</strong>, and <strong>ping</strong>.</p><pre>// internal/matchmaking/elo.go<br><br>func CanMatch(p1, p2 player.Player) bool {<br>// Skip if both players have different regions<br> if p1.Region != p2.Region {<br>  fmt.Printf(&quot;[Skip] Region mismatch: %s vs %s\n&quot;, p1.Region, p2.Region)<br>  return false<br> }<br><br> mmrGap := math.Abs(float64(p1.MMR - p2.MMR))<br> waitTime := float64(time.Now().Unix() - p1.JoinedAt)<br> maxGap := 100 + (waitTime/30)*10<br>// skip if there is too much Gap between Players MMR<br> if mmrGap &gt; maxGap {<br>  fmt.Printf(&quot;[Skip] MMR gap too high (%f &gt; %f) between %s and %s\n&quot;, mmrGap, maxGap, p1.Player_id, p2.Player_id)<br>  return false<br> }<br>// Skip if there are high ping of any player<br> if p1.Ping &gt; 100 || p2.Ping &gt; 100 {<br>  fmt.Printf(&quot;[Skip] Ping too high: %s (%d ms), %s (%d ms)\n&quot;, p1.Player_id, p1.Ping, p2.Player_id, p2.Ping)<br>  return false<br> }<br><br> fmt.Printf(&quot;[Match ✅] %s vs %s | MMR Gap: %.0f | Region: %s\n&quot;, p1.Player_id, p2.Player_id, mmrGap, p1.Region)<br> return true<br>}</pre><p>The CanMatch function returns true if the players meet the criteria for matchmaking, otherwise false.</p><p>Once two players can be matched, we publish the match to a <strong>Redis Pub/Sub topic</strong> so other services (like Notification Service or Game Orchestrator) can react to it.</p><pre>// internal/matchmaking/elo.go<br>func FormMatch(p1, p2 player.Player, redisClient *redis.Client) {<br> matchId := uuid.New().String()<br> ctx := context.Background()<br><br> match := Match{<br>  Id:      matchId,<br>  Players: []player.Player{p1, p2},<br>  Region:  p1.Region,<br> }<br><br> jsonData, err := json.Marshal(match)<br> if err != nil {<br>  fmt.Printf(&quot;[❌ ERROR] Failed to marshal match: %v\n&quot;, err)<br>  return<br> }<br> redisClient.Set(ctx, &quot;match:&quot;+matchId, jsonData, 0)<br><br> redisClient.Publish(ctx, &quot;matchmaking:events&quot;, jsonData)<br> redisClient.SAdd(ctx, &quot;match_players:&quot;+matchId, p1.Player_id, p2.Player_id)<br><br> err = redisClient.Publish(ctx, &quot;matchmaking:events&quot;, jsonData).Err()<br> if err != nil {<br>  fmt.Printf(&quot;[❌ ERROR] Failed to publish match: %v\n&quot;, err)<br>  return<br> }<br><br> fmt.Printf(&quot;[📣 MATCH PUBLISHED] Match ID: %s | Players: [%s, %s] | Region: %s\n&quot;,<br>  matchId, p1.Player_id, p2.Player_id, p1.Region)<br>}</pre><p>This publishes the match data to a Redis topic called <strong>matchmaking:events</strong>, so downstream services can process it.</p><p>We also need a function that removes the Player from the queue if they get a Match based on it Region, Ping and MMR.</p><pre>// internal/matchmaking/matchmaking.go<br><br>func RemoveFromQueue(ctx context.Context, redisClient *redis.Client, p1, p2 playerService.Player) {<br> queueKey := fmt.Sprintf(&quot;queue:%s:%s&quot;, p1.GameMode, p1.Region)<br><br> p1Json, err1 := json.Marshal(p1)<br> P2Json, err2 := json.Marshal(p2)<br><br> if err1 != nil || err2 != nil {<br>  fmt.Println(&quot;[❌ ERROR] Failed to marshal players during ZREM&quot;)<br>  return<br> }<br> removed, err := redisClient.ZRem(ctx, queueKey, string(p1Json), string(P2Json)).Result()<br><br> err = redisClient.Del(ctx, p1.Player_id).Err()<br> err = redisClient.Del(ctx, p2.Player_id).Err()<br><br> if err != nil {<br>  fmt.Printf(&quot;[❌ ERROR] Failed to remove players from queue: %v\n&quot;, err)<br>  return<br> }<br><br> fmt.Printf(&quot;[🗑️ Removed] %d player(s) removed from queue %s\n&quot;, removed, queueKey)<br><br>}</pre><p>RemoveFromQueue function removes the players from Queue based on their matchmaking.</p><p>Now, let’s implement the RunMatchmaking function. This is the <strong>main worker function</strong> that continuously monitors the player queue and runs the matchmaking algorithm.<br> It works in a loop, constantly checking for players and forming fair matches based on their MMR, region, and ping.</p><pre>// internal/matchmaking/matchmaking.go<br><br>func RunMatchmaking(ctx context.Context, redisClient redis.Client) {<br>    // Define supported regions and game modes<br>    regions := []string{&quot;asia&quot;, &quot;europe&quot;, &quot;na&quot;, &quot;sa&quot;}<br>    modes := []string{&quot;solo&quot;}<br><br>    for {<br>        // Iterate over each mode and region combination<br>        for _, mode := range modes {<br>            for _, region := range regions {<br><br>                // Construct the Redis sorted set key for the queue<br>                queueKey := fmt.Sprintf(&quot;queue:%s:%s&quot;, mode, region)<br><br>                // Retrieve all players currently waiting in the queue<br>                matchCandidates, err := redisClient.ZRange(ctx, queueKey, 0, -1).Result()<br>                if err != nil {<br>                    log.Printf(&quot;❌ Error reading from queue %s: %v&quot;, queueKey, err)<br>                    continue<br>                }<br><br>                // If no players are waiting, skip to the next queue<br>                if len(matchCandidates) == 0 {<br>                    log.Printf(&quot;ℹ️ No candidates in queue %s&quot;, queueKey)<br>                    continue<br>                }<br><br>                // Create a set to track players who have already been matched in this iteration<br>                matchedSet := make(map[string]bool)<br><br>                // Loop through all players to find potential matches<br>                for i := 0; i &lt; len(matchCandidates); i++ {<br>                    var player playerService.Player<br>                    // Deserialize the player data and skip if failed or already matched<br>                    if err := json.Unmarshal([]byte(matchCandidates[i]), &amp;player); err != nil || matchedSet[player.Player_id] {<br>                        continue<br>                    }<br><br>                    // Compare the current player with all subsequent players in the queue<br>                    for j := i + 1; j &lt; len(matchCandidates); j++ {<br>                        var candidate playerService.Player<br>                        // Deserialize candidate and skip if failed or already matched<br>                        if err := json.Unmarshal([]byte(matchCandidates[j]), &amp;candidate); err != nil || matchedSet[candidate.Player_id] {<br>                            continue<br>                        }<br><br>                        // If the player and candidate can be matched based on defined criteria<br>                        if CanMatch(player, candidate) {<br>                            log.Printf(&quot;✅ Match found: %s vs %s in %s (%s)&quot;, player.Player_id, candidate.Player_id, region, mode)<br><br>                            // Mark both players as matched to avoid duplicate matches<br>                            matchedSet[player.Player_id] = true<br>                            matchedSet[candidate.Player_id] = true<br><br>                            // Form a match (create match data, store it somewhere, notify players, etc.)<br>                            FormMatch(player, candidate, &amp;redisClient)<br><br>                            // Remove both players from the matchmaking queue<br>                            RemoveFromQueue(ctx, &amp;redisClient, player, candidate)<br><br>                            // Break inner loop since the player has been matched<br>                            break<br>                        }<br>                    }<br>                }<br><br>            }<br>        }<br><br>        // Sleep for a short interval before checking the queues again<br>        time.Sleep(3 * time.Second)<br>    }<br>}</pre><h3>Notification Service</h3><p>The primary role of the Notification Service is to <strong>notify players about important events from the matchmaking engine</strong>, such as when a match has been successfully found.</p><p>This service uses <strong>WebSocket connections</strong> to communicate with the players in real time. Each player establishes a WebSocket connection using their unique player_id, allowing the system to push notifications directly to the correct player.</p><p>In the background, the Notification Service <strong>subscribes to a Redis Pub/Sub topic called </strong><strong>&quot;game:allocated&quot;</strong>, which publishes player match events from the Matchmaking Service. Whenever a new match is created, the Notification Service receives the player data from this topic and sends a WebSocket message to notify the players involved.</p><p>So Let’s Start with ListenForMatches function which subscribes game:allocated topic and gets the complete match details based on the player&#39;s info.</p><pre>//internal/ws/pubSub.go<br><br>func ListenForMatches(ctx context.Context, rdb *redis.Client, manager *clients.Manager) {<br> sub := rdb.Subscribe(ctx, &quot;game:allocated&quot;)<br> ch := sub.Channel()<br><br> for msg := range ch {<br>  var payload gameorchestrator.GameSession<br><br>  if err := json.Unmarshal([]byte(msg.Payload), &amp;payload); err != nil {<br>   log.Println(&quot;❌ Failed to parse session payload:&quot;, err)<br>   continue<br>  }<br><br>  matchId := payload.MatchId<br>  address := fmt.Sprintf(&quot;%s:%d&quot;, payload.Address, payload.Port)<br><br>  // 🧠 Get full match data<br>  matchJSON, err := rdb.Get(ctx, &quot;match:&quot;+matchId).Result()<br>  if err != nil {<br>   log.Printf(&quot;❌ Could not fetch match data for match %s: %v&quot;, matchId, err)<br>   continue<br>  }<br><br>  var matchData map[string]interface{}<br>  if err := json.Unmarshal([]byte(matchJSON), &amp;matchData); err != nil {<br>   log.Printf(&quot;❌ Failed to unmarshal match data for match %s: %v&quot;, matchId, err)<br>   continue<br>  }<br><br>  // 🧩 Enrich match data with address and message<br>  matchData[&quot;address&quot;] = address<br>  matchData[&quot;message&quot;] = fmt.Sprintf(&quot;🎯 You’ve been matched!\n✅ Pod for match %s created at %s&quot;, matchId, address)<br>  matchData[&quot;type&quot;] = &quot;match_found&quot;<br><br>  // 🎯 Get matched player IDs<br>  playerIds, err := rdb.SMembers(ctx, &quot;match_players:&quot;+matchId).Result()<br>  if err != nil {<br>   log.Printf(&quot;❌ Could not get players for match %s: %v&quot;, matchId, err)<br>   continue<br>  }<br><br>  // 📨 Send to matched players only<br>  for _, playerId := range playerIds {<br>   conn := manager.Get(playerId)<br>   if conn == nil {<br>    log.Printf(&quot;⚠️ No active WebSocket connection for player %s&quot;, playerId)<br>    continue<br>   }<br><br>   if err := conn.WriteJSON(matchData); err != nil {<br>    log.Printf(&quot;❌ Failed to send match to player %s: %v&quot;, playerId, err)<br>   } else {<br>    log.Printf(&quot;📨 Sent full match info to player %s&quot;, playerId)<br>   }<br>  }<br> }<br>}</pre><p><strong>Handling Player Connections</strong></p><p>Now that we have a handler where players can establish a WebSocket connection, let’s break down how we keep track of these active players.</p><p>Whenever a player connects, we extract them player_id from the query params and upgrade the connection to WebSocket. Then, we store this connection for future use (for example, to notify them when a match is found).</p><pre>package ws<br><br>import (<br> &quot;log&quot;<br> &quot;net/http&quot;<br><br> &quot;github.com/Yashh56/matchmakingEngine/pkg/clients&quot;<br> &quot;github.com/gorilla/websocket&quot;<br>)<br><br>var upgrader = websocket.Upgrader{}<br><br>func HandleWebSocket(manager *clients.Manager) http.HandlerFunc {<br> return func(w http.ResponseWriter, r *http.Request) {<br>  playerID := r.URL.Query().Get(&quot;player_id&quot;)<br>  if playerID == &quot;&quot; {<br>   http.Error(w, &quot;player_id required&quot;, http.StatusBadRequest)<br>   return<br>  }<br><br>  conn, err := upgrader.Upgrade(w, r, nil)<br>  if err != nil {<br>   log.Println(&quot;WebSocket upgrade error:&quot;, err)<br>   return<br>  }<br><br>  manager.Add(playerID, conn)<br>  log.Printf(&quot;🟢 Player connected: %s\n&quot;, playerID)<br> }<br>}</pre><p>Managing Connected Players</p><p>But where exactly are we storing these player connections? For that, we use a simple <strong>Connection Manager</strong>, which acts as an in-memory registry of all connected WebSocket clients.</p><p>This manager allows us to:</p><ul><li><strong>Add</strong> a new player’s connection when they join.</li><li><strong>Retrieve</strong> a player’s connection by their ID when sending them a message.</li><li><strong>Remove</strong> a player’s connection when they disconnect.</li><li><strong>List all connected players</strong> (useful for broadcasting or debugging).</li></ul><pre>// package/clients/manager.go<br>package clients<br><br>import (<br> &quot;sync&quot;<br><br> &quot;github.com/gorilla/websocket&quot;<br>)<br><br>type Manager struct {<br> mu      sync.RWMutex<br> clients map[string]*websocket.Conn<br>}<br><br>func NewManager() *Manager {<br> return &amp;Manager{<br>  clients: make(map[string]*websocket.Conn),<br> }<br>}<br><br>func (m *Manager) Add(id string, conn *websocket.Conn) {<br> m.mu.Lock()<br> defer m.mu.Unlock()<br> m.clients[id] = conn<br>}<br><br>func (m *Manager) Get(id string) *websocket.Conn {<br> m.mu.RLock()<br> defer m.mu.RUnlock()<br> return m.clients[id]<br>}<br><br>func (m *Manager) GetAllClients() map[string]*websocket.Conn {<br> m.mu.RLock()<br> defer m.mu.RUnlock()<br><br> // Return a copy to avoid race conditions<br> copied := make(map[string]*websocket.Conn)<br> for id, conn := range m.clients {<br>  copied[id] = conn<br> }<br> return copied<br>}<br><br>func (m *Manager) Remove(id string) {<br> m.mu.Lock()<br> defer m.mu.Unlock()<br> delete(m.clients, id)<br>}</pre><h3>🎉 Hooray! We’re Done</h3><p>And that’s it — our simple yet functional matchmaking engine is ready to go!</p><p>Of course, this is just a <strong>basic implementation</strong> to demonstrate the matchmaking process. In a real-world scenario, you could make it much more robust, adding:</p><ul><li>Advanced matchmaking algorithms</li><li>Scalable microservices</li><li>Dynamic game server orchestration</li><li>Fault tolerance and retries</li><li>Distributed queue handling</li></ul><p>Here are the Full Code:</p><p><a href="https://github.com/Yashh56/matchmakingEngine">GitHub - Yashh56/matchmakingEngine: A scalable real-time matchmaking engine built using Go, Redis, WebSockets, and Kubernetes (via kind or minikube) to simulate game pod creation and player assignment in a distributed environment. This system mimics real-world multiplayer games&#39; server orchestration using custom services and Kubernetes pods.</a></p><h3>🚀 What’s Next?</h3><p>You can extend this project yourself, or just try running the complete version from the GitHub repo.</p><p>Once everything is running, you’ll start seeing real-time match events like this:</p><pre>📨 [14:30:25] New Message Received<br>────────────────────────────────────────────────────────────<br>🎯 MATCH FOUND!<br><br>📋 Match ID: bf93e3f1-5517-4d8f-a162-44400a40fb30<br>🗺️  Region: ASIA<br>👥 Players:<br>   Player 1:<br>     🆔 ID: 1<br>     ⭐ MMR: 1000<br>     📶 Ping: 30ms<br>     🎮 Mode: solo<br>     🌍 Region: asia<br>     ⏰ Joined: 14:30:20<br><br>   Player 2:<br>     🆔 ID: 3<br>     ⭐ MMR: 1000<br>     📶 Ping: 30ms<br>     🎮 Mode: solo<br>     🌍 Region: asia<br>     ⏰ Joined: 14:30:24<br><br>💬 Message: 🎯 You&#39;ve been matched!<br>────────────────────────────────────────────────────────────</pre><p>Thanks for following along. Hope this gave you a clear understanding of how matchmaking works in online games and how you can build scalable systems using Go and Redis.</p><p>Feel free to fork the repo, build upon this, and maybe even deploy your matchmaking system for your game projects.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=96e10f095ce1" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>