Skip to content

paulcaru/PlayerWallet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Player Wallet Microservice

A .NET 10 microservice that manages player wallet balances for a PAM (Player Account Management) platform. Built with .NET Aspire, Microsoft Orleans, PostgreSQL, and Kafka.


Architecture

  • Orleans — actor-model grain per player (PlayerWalletGrain). One activation cluster-wide eliminates balance race conditions without locks.
  • PostgreSQL — grain state (JSON via ADO.NET provider), transaction audit log (wallet_transactions), balance projection (wallet_balances), and Orleans clustering membership.
  • Kafka — wallet domain events (FundsAdded, FundsDeducted, DeductionRejected) published to wallet-events.
  • Aspire — orchestrates Postgres and Kafka containers in development; injects typed connection strings.
  • Clean Architecture — Domain → Application → Infrastructure / Api. MediatR CQRS, FluentValidation pipeline behaviour, ErrorOr result type in handlers.

See architecture.md for component diagrams and decisions.md for ADRs.


Prerequisites


Running the Service

dotnet run --project src/PlayerWallet.AppHost

Aspire starts Postgres, Kafka, the one-shot migrator, and the API. The dashboard is available at https://localhost:15888 (port may vary — check the AppHost console output).

Swagger UI: https://localhost:<api-port>/swagger


API

All endpoints require X-Player-Id (UUID). Mutating endpoints require Idempotency-Key (UUID v4).

Method Path Description
POST /api/v1/wallets/deposit Credit player balance
POST /api/v1/wallets/withdraw Debit player balance
GET /api/v1/wallets/balance Read current balance

Deposit / Withdraw request body

{ "amount": 10.00, "currencyCode": "USD" }

Headers

X-Player-Id:      <player-guid>          (required on all endpoints)
Idempotency-Key:  <uuid-v4>              (required on POST)
X-Correlation-Id: <uuid-v4>              (optional — generated server-side if absent)

Project Structure

src/
├── PlayerWallet.AppHost/        # Aspire orchestrator
├── PlayerWallet.ServiceDefaults/ # OTEL, health checks, resilience
├── PlayerWallet.Api/            # Minimal API endpoints, middleware, Swagger
├── PlayerWallet.Application/    # MediatR commands/queries, FluentValidation
├── PlayerWallet.Domain/         # Grain interfaces, events, exceptions
├── PlayerWallet.Grains/         # Orleans grain implementations
├── PlayerWallet.Infrastructure/ # Dapper repos, Kafka producer, DbUp migrations
└── PlayerWallet.Migrator/       # One-shot worker: runs DbUp then exits

tests/
├── PlayerWallet.ComponentTests/ # xUnit + Testcontainers (33 tests)
└── PlayerWallet.Benchmarks/     # NBomber sustained-load benchmarks

Tests

dotnet test tests/PlayerWallet.ComponentTests

33 component tests covering deposit, withdraw, balance, idempotency, error cases, and Kafka event publishing. Postgres and Kafka run in Testcontainers — no manual setup required.


Benchmarks

1 000 rps × 5 minutes per scenario, 300 000 requests, zero errors.

# Run individually — each scenario takes ~6 minutes
dotnet test tests/PlayerWallet.Benchmarks --filter "Balance_1000rps_5min_MeetsSLA"
dotnet test tests/PlayerWallet.Benchmarks --filter "Deposit_1000rps_5min_MeetsSLA"
dotnet test tests/PlayerWallet.Benchmarks --filter "Withdraw_1000rps_5min_MeetsSLA"
Scenario Mean StdDev p95 p99 SLA (p99 < 100 ms)
Balance 1.00 ms 2.20 ms 1.64 ms 12.07 ms
Deposit 98.24 ms 66.57 ms 225.41 ms 338.18 ms
Withdraw 108.54 ms 84.73 ms 260.35 ms 434.43 ms

Key Design Decisions

Decision Choice Why
Idempotency L1 in-memory cache + L2 INSERT ... ON CONFLICT DO NOTHING RETURNING * Two-layer, one DB round-trip, no extra infrastructure
Persistence Dapper (runtime) + DbUp (migrations) Minimal overhead at 15K writes/s; DbUp handles Orleans tables EF Core cannot
Clustering Orleans ADO.NET (all environments) Same code path in dev and production
Error handling Exceptions at grain boundary, ErrorOr in handlers Orleans serialiser constraint on grain return types
Kafka delivery Direct from grain, CDC-ready outbox Transactional outbox requires sharing a DB transaction with WriteStateAsync, which the ADO.NET provider does not support

See decisions.md for full ADRs with alternatives and tradeoffs.

About

Player Wallet Microservice

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors