{"id":17803445,"url":"https://github.com/mehdihadeli/game-leaderboard-microservices","last_synced_at":"2025-04-30T15:24:20.152Z","repository":{"id":185400625,"uuid":"666176194","full_name":"mehdihadeli/game-leaderboard-microservices","owner":"mehdihadeli","description":"🎮 Implementation of an imaginary Game Leader Board application, based on Microservices Architecture, Event Driven Architecture, Vertical Slice Architecture, Event Sourcing with EventStoreDB, Redis SortedSet, Redis Pub/Sub, SignalR and .Net 8.","archived":false,"fork":false,"pushed_at":"2024-07-27T13:49:58.000Z","size":15906,"stargazers_count":55,"open_issues_count":0,"forks_count":13,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-30T15:24:12.215Z","etag":null,"topics":["aspnetcore","cqrs","dotnet","event-driven-architecture","eventsourcing","eventstoredb","masstransit","message-broker","message-bus","microservice-example","microservices","pub-sub","redis","vertical-slice-architecture"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mehdihadeli.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-13T22:30:34.000Z","updated_at":"2025-04-19T16:59:12.000Z","dependencies_parsed_at":"2024-02-02T00:08:46.389Z","dependency_job_id":"4be6bd28-ebf5-49b4-8287-4047037f8846","html_url":"https://github.com/mehdihadeli/game-leaderboard-microservices","commit_stats":null,"previous_names":["mehdihadeli/game-leaderboard-microservices"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehdihadeli%2Fgame-leaderboard-microservices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehdihadeli%2Fgame-leaderboard-microservices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehdihadeli%2Fgame-leaderboard-microservices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehdihadeli%2Fgame-leaderboard-microservices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mehdihadeli","download_url":"https://codeload.github.com/mehdihadeli/game-leaderboard-microservices/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251730068,"owners_count":21634319,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aspnetcore","cqrs","dotnet","event-driven-architecture","eventsourcing","eventstoredb","masstransit","message-broker","message-bus","microservice-example","microservices","pub-sub","redis","vertical-slice-architecture"],"created_at":"2024-10-27T12:46:48.429Z","updated_at":"2025-04-30T15:24:20.128Z","avatar_url":"https://github.com/mehdihadeli.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Game LeaderBoard Microservices\n\n[![CI](https://github.com/mehdihadeli/game-leaderboard-microservices/actions/workflows/ci.yml/badge.svg)](https://github.com/mehdihadeli/game-leaderboard-microservices/actions/workflows/ci.yml)\n\n[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/mehdihadeli/game-leaderboard-microservices)\n\nThis project is an implementation of an imaginary [Game LeaderBoard](https://en.wikipedia.org/wiki/Ladder_tournament) application, based on Microservices Architecture, [Event Driven Architecture](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven), [Vertical Slice Architecture](https://jimmybogard.com/vertical-slice-architecture/), [Event Sourcing](https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing) with EventStoreDB, [Redis SortedSet](https://redis.io/docs/data-types/sorted-sets/), [Redis Pub/Sub](https://redis.io/docs/interact/pubsub/), SignalR and .Net 8.\n\nThis application capable of handling online calculation of player ranks with using Redis `SortedSet` so it is very fast and capable for handling 1 million request per second.\n\n![](./assets/leaderboard.gif)\n\n## Prerequisites\n\n## Features\n\n- ✅ Using `Vertical Slice Architecture` as a high level architecture\n- ✅ Using `Event Driven Architecture` and asynchronous communications on top of RabbitMQ Message Broker and MassTransit\n- ✅ Using `Outbox Pattern` for all microservices for [Guaranteed Delivery](https://www.enterpriseintegrationpatterns.com/GuaranteedMessaging.html) or [At-least-once Delivery](https://www.cloudcomputingpatterns.org/at_least_once_delivery/) And Using [Inbox Pattern](https://learn.microsoft.com/en-us/azure/service-bus-messaging/duplicate-detection) for handling [Idempotency](https://www.cloudcomputingpatterns.org/idempotent_processor/) in receiver side and [Exactly-once Delivery](https://www.cloudcomputingpatterns.org/exactly_once_delivery/)\n- ✅ Using `CQRS Pattern` on top of `MediatR` library\n- ✅ Using `Minimal APIs` for handling requests\n- ✅ Using [Redis SortedSet](https://redis.io/docs/data-types/sorted-sets) for calculating player ranks\n- ✅ Using [Redis Pub/Sub](https://redis.io/docs/interact/pubsub/) for some of asynchronous communications\n- ✅ Using Event Sourcing and [EventStoreDB](https://www.eventstore.com/eventstoredb) as our primary database\n- ✅ Using [Postgres](https://www.npgsql.org/efcore/) and Redis as secondary database on top of EventStore [Projections](https://web.archive.org/web/20230128040244/https://zimarev.com/blog/event-sourcing/projections/)\n- ✅ Supporting different type of caching strategy like `Read-Through`, `Write-Through`, `Write-Behind`, `Read and Write Cache Aside` on top of `redis` for handling millions of request per second\n\n## Libraries\n\n- ✔️ **[`.NET 8`](https://dotnet.microsoft.com/download)** - .NET Framework and .NET Core, including ASP.NET and ASP.NET Core\n- ✔️ **[`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis)** - General purpose redis client\n- ✔️ **[`MassTransit`](https://github.com/MassTransit/MassTransit)** - Distributed Application Framework for .NET\n- ✔️ **[`EventStore-Client-Dotnet`](https://github.com/EventStore/EventStore-Client-Dotnet)** - Dotnet Client SDK for the Event Store gRPC Client API written in C#\n- ✔️ **[`Npgsql Entity Framework Core Provider`](https://www.npgsql.org/efcore/)** - Npgsql has an Entity Framework (EF) Core provider. It behaves like other EF Core providers (e.g. SQL Server), so the general EF Core docs apply here as well\n- ✔️ **[`FluentValidation`](https://github.com/FluentValidation/FluentValidation)** - Popular .NET validation library for building strongly-typed validation rules\n- ✔️ **[`Swagger \u0026 Swagger UI`](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)** - Swagger tools for documenting API's built on ASP.NET Core\n- ✔️ **[`Serilog`](https://github.com/serilog/serilog)** - Simple .NET logging with fully-structured events\n- ✔️ **[`Polly`](https://github.com/App-vNext/Polly)** - Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner\n- ✔️ **[`Scrutor`](https://github.com/khellang/Scrutor)** - Assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection\n- ✔️ **[`Newtonsoft.Json`](https://github.com/JamesNK/Newtonsoft.Json)** - Json.NET is a popular high-performance JSON framework for .NET\n- ✔️ **[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks)** - Enterprise HealthChecks for ASP.NET Core Diagnostics Package\n  NET Compiler Platform\n- ✔️ **[`AutoMapper`](https://github.com/AutoMapper/AutoMapper)** - Convention-based object-object mapper in .NET.\n\n## Architecture\n\nFor implementing this application we can use different type of caching strategy and we can config our caching strategy in [appsettings.json](src/Server/Services/LeaderBoard.GameEventsProcessor/appsettings.json) file of our [GameEventsProcessor](src/Server/Services/LeaderBoard.GameEventsProcessor/) service and run our [caching strategy workers](src/Server/CacheStrategies/) separately (like WriteThrough, WriteBehind and ReadThrough), if we don't want to use our built-in `Write-aside caching` and ` Read-aside caching` caching strategy.\n\nFor decreasing calculation and response time for real-time rank calculation with millions of request and changes per second we need to use a high performant approach to handling this issue, redis has very handy feature of [SortedSet](https://redis.io/docs/data-types/sorted-sets/) and when store a member with specific score, based on score sorted re-arrange affected members with new rank for each member in the SortedSet. With sorted sets it is trivial to return a list of player sorted by their scores because actually they are already sorted and ranked.\n\nEvery time we add an element Redis performs an maximum `O(log(N))` operations, where n is the number of members, to re-sort and re-rank affected elements based on new element score. after that when we ask for sorted elements Redis does not have to do any work at all, it's `already all sorted` and drastically decrease our reading times and reading hits (It performs a binary search-like operation to locate the element efficiently, resulting in a time complexity of O(log N)).\n\n- Getting the score of an element: O(1)\n- Retrieving an element by its rank: O(log N)\n\nAlso for ensuring about losing our data and events in our redis cache because it is on the ram, we need to have a primary database and because we want to keep track of all of our events over time we use EventStoreDB as our primary storage and based on caching-strategies on the `write` and `read` level we update our primary database and secondary redis database and postgres database (using EventStore projections for updating secondary databases).\n\n### Write-Aside Caching \u0026 Read-Aside Caching\n\n![](./assets/write-read-cache-aside.png)\n\nHere we used `Cache-Aside` strategy for both read and write.\n\nThe flow of our application for showing leader board to users is according these steps:\n\n1. Suppose we have a online game and our users can play the game through mobile or web browser. After getting some points in the game our `mobile app` or `web app` will send a `AddOrUpdate` command to its corresponding endpoint in `GameEventSource` service through our `traefik ingress`, load balancer and reverse proxy.\n2. Our traefik will route `AddOrUpdate` request to `GameEventSource` service endpoint.\n3. AddOrUpdate endpoint `GameEventSource` service publishes `GameEventChanged` to the broker.\n4. `GameEventChangedConsumer` which is subscribed on `GameEventChanged` event in `GameEventProcessor` service, will get `GameEventChanged` event from the broker.\n5. our `GameEventChangedConsumer` will call `AddOrUpdatePlayerScore` command and inner `AddOrUpdatePlayerScoreHandler` handler we store events on the EventStoreDB for keep track of all events over the time.\n6. After storing events on EventStoreDB our `Postgres Projection (EFCorePlayerScoreReadModelProjection)` and `Redis Projection (RedisPlayerScoreReadModelProjection)` will be triggered.Then these projections will materialize the input data into their respective read data models and store them on Redis and Postgres.\n7. Our `RedisPlayerScoreReadModelProjection` projection will publish a `RedisScoreChangedMessage` message through Redis `Pub/Sub`\n8. Our `GameEventProcessor` service, which is subscribed on `RedisScoreChangedMessage` Redis message, will get message by its predefined `Redis subscriber` on `RedisScoreChangedMessage` message.\n9. Our `Redis Subscriber` on `RedisScoreChangedMessage` message will publish `PlayersRankAffected` message to the broker.\n10. Our SignalR service which is subscribed on `PlayersRankAffected` message through `PlayersRankAffectedConsumer` consumer, will get the message and calls `UpdatePlayersScoreForClient` on our `IHubService`.\n11. Our `UpdatePlayersScoreForClient` on `IHubService` of SignalR service, will get all affected players based on our `ScoreChanged` event through a REST call to `GameEventProcessor` service.\n12. Our `GameEventProcessor` service and `GetPlayerGroupGlobalScoresAndRanks` endpoint will get all related players score with `GetGlobalScoreAndRank` query. This query at-first tries to get rank and score form redis sorted set and if not exists it will uses `Read-Aside Caching` and will read data from primary database and will update our redis database.\n13. If the data not existed on the redis we check our primary database which is postgres in this example.\n14. After getting data from postgres we update our Redis SortedSet and HashSet data.\n15. We send fetched score via `HubService` of our SignalR service in a real time to connected affected players.\n\n### Write-Through \u0026 Read-Through\n\nTODO\n\n### Write-Behind \u0026 Read-Through\n\nTODO\n\n## Application Structure\n\nIn this project I used [vertical slice architecture](https://jimmybogard.com/vertical-slice-architecture/) or [Restructuring to a Vertical Slice Architecture](https://codeopinion.com/restructuring-to-a-vertical-slice-architecture/) also I used [feature folder structure](http://www.kamilgrzybek.com/design/feature-folders/) in this project.\n\n- We treat each request as a distinct use case or slice, encapsulating and grouping all concerns from front-end to back.\n- When We adding or changing a feature in an application in n-tire architecture, we are typically touching many different \"layers\" in an application. we are changing the user interface, adding fields to models, modifying validation, and so on. Instead of coupling across a layer, we couple vertically along a slice and each change affects only one slice.\n- We `Minimize coupling` `between slices`, and `maximize coupling` `in a slice`.\n- With this approach, each of our vertical slices can decide for itself how to best fulfill the request. New features only add code, we're not changing shared code and worrying about side effects. For implementing vertical slice architecture using cqrs pattern is a good match.\n\n![](./assets/vertical-slice-architecture.jpg)\n\nAlso here I used [CQRS](https://www.eventecommerce.com/cqrs-pattern) for decompose my features to very small parts that makes our application:\n\n- maximize performance, scalability and simplicity.\n- adding new feature to this mechanism is very easy without any breaking change in other part of our codes. New features only add code, we're not changing shared code and worrying about side effects.\n- easy to maintain and any changes only affect on one command or query (or a slice) and avoid any breaking changes on other parts\n- it gives us better separation of concerns and cross cutting concern (with help of MediatR behavior pipelines) in our code instead of a big service class for doing a lot of things.\n\nWith using [CQRS](https://event-driven.io/en/cqrs_facts_and_myths_explained/), our code will be more aligned with [SOLID principles](https://en.wikipedia.org/wiki/SOLID), especially with:\n\n- [Single Responsibility](https://en.wikipedia.org/wiki/Single-responsibility_principle) rule - because logic responsible for a given operation is enclosed in its own type.\n- [Open-Closed](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) rule - because to add new operation you don’t need to edit any of the existing types, instead you need to add a new file with a new type representing that operation.\n\nHere instead of some [Technical Splitting](http://www.kamilgrzybek.com/design/feature-folders/) for example a folder or layer for our `services`, `controllers` and `data models` which increase dependencies between our technical splitting and also jump between layers or folders, We cut each business functionality into some vertical slices, and inner each of these slices we have [Technical Folders Structure](http://www.kamilgrzybek.com/design/feature-folders/) specific to that feature (command, handlers, infrastructure, repository, controllers, data models, ...).\n\n## Prerequisites\n\n1. Install git - [https://git-scm.com/downloads](https://git-scm.com/downloads).\n2. Install .NET Core 8.0 - [https://dotnet.microsoft.com/en-us/download/dotnet/8.0](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).\n3. Install Visual Studio, Rider or VSCode.\n4. Install docker - [https://docs.docker.com/docker-for-windows/install/](https://docs.docker.com/docker-for-windows/install/).\n5. Make sure that you have ~10GB disk space.\n6. Clone Project [https://github.com/mehdihadeli/leaderboard](https://github.com/mehdihadeli/leaderboard), make sure that's compiling\n7. Run the [docker-compose.infrastructure.yaml](./docker-compose.infrastructure.yaml) file, for running prerequisites infrastructures with `docker-compose -f ./docker-compose.infrastructure.yaml up -d` command.\n8. Open [leaderboard.sln](./leaderboard.sln) solution.\n\n## How To Run FrontEnd\n\nFor implementing our frontend we used Angular and for real-time communication with SignalR Hub we used [@microsoft/signalr](https://www.npmjs.com/package/@microsoft/signalr) library.\n\nFor running our front-end App:\n\n1. go to `cd src/Client` folder and open it in VSCode:\n\n```bash\ncd src/Client\ncode .\n```\n\n2. Install node modules:\n\n```bash\nnpm install\n```\n\n3. Run front end:\n\n```bash\nnpm start\n```\n\n## How To Run Backend\n\nFor running our backend we can use different caching strategies:\n\n### Running With Cache Aside Strategies\n\nFirst of all we should turn-on both write and read cache aside strategies in `GameEventProcessor` service and [appsettings.json](src/Server/Services/LeaderBoard.GameEventsProcessor/appsettings.json) file with setting `UseReadCacheAside` and `UseWriteCacheAside` to `true`:\n\n```json\n  \"LeaderBoardOptions\": {\n    \"UseReadCacheAside\": true,\n    \"UseWriteCacheAside\": true,\n    \"UseReadThrough\": false,\n    \"UseWriteBehind\": false,\n    \"UseWriteThrough\": false,\n    \"CleanupRedisOnStart\": true,\n    \"UseCacheWarmUp\": true,\n    \"SeedInitialData\": true\n  },\n```\n\nNow we should run our needed services:\n\n```bash\ndotnet run --project src/Server/Services/LeaderBoard.GameEventsSource\ndotnet run --project src/Server/Services/LeaderBoard.GameEventsProcessor\ndotnet run --project src/Server/Services/LeaderBoard.SignalR\n```\n\nNow our `GameEventSource` service is available on [`http://localhost:3500`](http://localhost:3500), and `GameEventsProcessor` service is available on [`http://localhost:5000`](http://localhost:5000) and our SignalR is available on [`http://localhost:7200`](http://localhost:7200).\n\n### Running With Write Behind And ReadThrough Strategies\n\nFirst of all we should turn-on both write-behind and read-through strategies in `GameEventProcessor` service and [appsettings.json](src/Server/Services/LeaderBoard.GameEventsProcessor/appsettings.json) file with setting `UseWriteBehind` and `UseReadThrough` to `true`:\n\n```json\n  \"LeaderBoardOptions\": {\n    \"UseReadCacheAside\": false,\n    \"UseWriteCacheAside\": false,\n    \"UseReadThrough\": true,\n    \"UseWriteBehind\": true,\n    \"UseWriteThrough\": false,\n    \"CleanupRedisOnStart\": true,\n    \"UseCacheWarmUp\": true,\n    \"SeedInitialData\": true\n  },\n```\n\nNow we should run our needed services:\n\n```bash\ndotnet run --project src/Server/CacheStrategies/LeaderBoard.ReadThrough\ndotnet run --project src/Server/CacheStrategies/LeaderBoard.WriteBehind\ndotnet run --project src/Server/Services/LeaderBoard.GameEventsSource\ndotnet run --project src/Server/Services/LeaderBoard.GameEventsProcessor\ndotnet run --project src/Server/Services/LeaderBoard.SignalR\n```\n\nNow our `GameEventSource` service is available on [`http://localhost:3500`](http://localhost:3500), and `GameEventsProcessor` service is available on [`http://localhost:5000`](http://localhost:5000) and our SignalR is available on [`http://localhost:7200`](http://localhost:7200).\n\n## Contribution\n\nThe application is in development status. You are feel free to submit pull request or create the issue.\n\n## License\n\nThe project is under [MIT license](https://github.com/mehdihadeli/game-leaderboard-microservices/blob/main/LICENSE).\n\n## References\n\n- [3 crucial caching choices: Where, when, and how](https://www.gomomento.com/blog/3-crucial-caching-choices-where-when-and-how)\n- [6 common caching design patterns to execute your caching strategy](https://www.gomomento.com/blog/6-common-caching-design-patterns-to-execute-your-caching-strategy)\n- [Caching Strategies and How to Choose the Right One](https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/)\n- [oskardudycz/EventSourcing.NetCore](https://github.com/oskardudycz/EventSourcing.NetCore)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmehdihadeli%2Fgame-leaderboard-microservices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmehdihadeli%2Fgame-leaderboard-microservices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmehdihadeli%2Fgame-leaderboard-microservices/lists"}