Play Economy: A Distributed Game Economy

Play Economy: A Distributed Game Economy

A microservices-based game economy platform where users browse a catalog, purchase items with in-game currency, and manage inventory. Demonstrating distributed system patterns like saga orchestration, event-driven communication, and OAuth 2.0 authentication.

M
mark.ambro
March 08, 2026

The Challenge

Monolithic architectures struggle as systems grow: a single deployment bottleneck, tightly coupled domains, and no clear ownership boundaries. I wanted to build a system that tackled these problems head-on. Multiple services that own their own data, communicate asynchronously, and handle failure gracefully.

The core problem: how do you coordinate a multi-step purchase transaction across independent services without a shared database? A user buying an item requires checking the catalog for pricing, debiting currency from their account, and granting the item to their inventory. Three services that each own a piece of the workflow. If any step fails, the others need to compensate. Getting this wrong means users lose currency without receiving items, or receive items without paying.

Beyond the transaction challenge, the system needed proper authentication and authorization (not every user should manage the catalog), resilient inter-service communication (one service going down shouldn't cascade), and a frontend that ties it all together seamlessly.

The Approach

I followed the Building Microservices with .NET professional development guide by Julio Casal. Rather than building a toy CRUD app, the course builds a realistic distributed system incrementally — starting with a single service and layering in complexity.

Key architectural decisions:

  • Service boundaries by domain: Each service owns its domain — Catalog manages items, Identity manages users and currency, Inventory tracks ownership, and Trading orchestrates purchases. No shared databases.

  • Asynchronous messaging over synchronous calls: RabbitMQ with MassTransit handles inter-service events. When a catalog item is created or updated, consumers in other services sync their local caches. This keeps services decoupled and resilient to temporary outages.

  • Saga pattern for distributed transactions: The purchase workflow uses a state machine (Automatonymous) to orchestrate the multi-step process: calculate total, debit currency, grant items. If debiting fails (insufficient funds), the saga moves to a faulted state — no partial transactions.

  • Resilience-first communication: Synchronous HTTP calls between services use Polly with exponential backoff retries and circuit breaker policies. If a downstream service is unhealthy, the circuit opens after 3 failures and stops hammering it.

  • OAuth 2.0 / OpenID Connect: Duende IdentityServer provides centralized authentication. Services validate JWT bearer tokens independently. Role-based and scope-based authorization controls who can manage the catalog vs. who can only browse.

The Solution

Solution illustration

The system comprises four .NET microservices, a shared common library, and a React frontend:

Play.Catalog: REST API for item management (CRUD). It publishes CatalogItemCreated, CatalogItemUpdated, and CatalogItemDeleted events to RabbitMQ.

Play.Identity: OAuth 2.0 authorization server built on Duende IdentityServer with MongoDB-backed ASP.NET Identity.

Play.Inventory: Tracks item ownership per user. Maintains a local cache of catalog items by consuming catalog events, so it never depends on synchronous calls to the Catalog service during reads.

Play.Trading: The primary orchestrator with a saga that coordinates the entire purchase flow:

  1. Receives PurchaseRequested → moves to Accepted

  2. Runs CalculatePurchaseTotalActivity (quantity × item price)

  3. Requests currency debit from Identity and item grant from Inventory

  4. Transitions to Completed or Faulted based on outcomes

Play.Common: Shared NuGet-style library providing generic IRepository<T> abstractions, MongoDB configuration, MassTransit + RabbitMQ setup with retry policies, and JWT bearer authentication — this helps reduce boilerplate across services.

Play.Frontend: React SPA built with Vite. Features catalog browsing, inventory viewing, user management (admin only), and purchase submission. OIDC authentication with silent sign-in, popup, and redirect fallback strategies.

Infrastructure: Docker Compose running locally.

Results

The outcome? Four independently deployable microservices communicating through well-defined contracts. No shared state, no distributed monolith.

  • Zero partial transactions in the purchase workflow thanks to saga orchestration with compensation logic. Insufficient funds → faulted state, no items granted.

  • Fault-tolerant communication is managed via a circuit breaker and exponential backoff, protecting the system from overload during service restoration.

  • Decoupled data synchronization: Inventory and Trading services maintain local caches of catalog data, enabling reads even when the Catalog service is temporarily unavailable.

  • Secure by default — every API endpoint requires a valid JWT. Admin operations are restricted by role and scope claims.

I got a lot of hands-on experience with patterns that directly apply to production distributed systems: event sourcing fundamentals, saga orchestration, eventual consistency, and resilience engineering. This was a tough one and I'm glad I pulled through!