A concert ticket ordering system built to learn and get hands-on experience with microservices, gRPC, message queues, and GraphQL. The domain is intentionally simple so the focus stays on the architecture and the technologies — each service is independently deployable and the whole system is designed with Kubernetes in mind as a future deployment target.
The system covers the full lifecycle of a ticket purchase: browsing concerts, placing an order, authentication, and asynchronous notifications.
┌─────────────────────────────────────┐
│ External Clients │
└──────────────────┬──────────────────┘
│ GraphQL / HTTP
┌──────────▼──────────┐
│ Gateway Service │
│ (HotChocolate) │
└──┬───────────────┬───┘
gRPC │ │ HTTP
┌────────────────────▼─┐ ┌─────▼────────────┐
│ Catalog Service │ │ Identity Service │
│ (gRPC + Redis) │ │ (JWT + Refresh) │
└──────────────────────┘ └──────────────────┘
REST
┌────────────────────────────────────────────────┐
│ Ordering Service │
│ (REST API + MassTransit) │
└────────────────────────┬───────────────────────┘
│ OrderCreatedEvent
┌─────────▼──────────────┐
│ RabbitMQ (AMQP) │
└─────────┬───────────────┘
│
┌─────────────▼──────────────┐
│ Notifications Service │
│ (MassTransit Consumer) │
└────────────────────────────┘
PostgreSQL ×3 Redis RabbitMQ
(catalog / ordering (catalog cache) (event bus)
/ identity DBs)
All infrastructure and services are orchestrated by .NET Aspire — no Docker Compose required. Each service exposes /health and /alive endpoints and is stateless by design, making the system straightforward to deploy to Kubernetes in the future.
| Service | Protocol | Responsibility |
|---|---|---|
| Gateway | GraphQL (HotChocolate) | Single entry point; composes Catalog (gRPC) and Identity (HTTP) |
| Catalog | gRPC | Exposes concert listings with Redis-backed caching (30s TTL) |
| Identity | REST | User registration, JWT issuance, single-use refresh token rotation |
| Ordering | REST | Order creation, persistence, and event publishing |
| Notifications | Worker (MassTransit) | Consumes OrderCreatedEvent from RabbitMQ asynchronously |
| ServiceDefaults | Library | Shared cross-cutting concerns: telemetry, health checks, auth, error handling |
Runtime & Framework
- .NET 10 / C# — nullable reference types, minimal hosting model
- .NET Aspire 13 — type-safe infrastructure-as-code, built-in service discovery
Communication
- gRPC (Grpc.AspNetCore 2.76) — strongly-typed internal service calls via Protocol Buffers
- GraphQL (HotChocolate 15.1) — flexible public API with authorization middleware
- MassTransit 8.3 + RabbitMQ — decoupled async event publishing and consumption
Data
- PostgreSQL — three isolated databases (catalog, ordering, identity), managed by EF Core 9
- Redis — distributed cache for the Catalog service
Security
- ASP.NET Core Identity + EF Core — user management, password hashing
- JWT Bearer (HS256) — stateless access tokens with configurable issuer/audience
- Refresh token rotation — single-use refresh tokens stored in Postgres with revocation tracking
Observability
- OpenTelemetry 1.15 — distributed tracing, metrics, and structured logging across all services
- OTLP exporter — pluggable; connects to Aspire dashboard or any OTEL-compatible backend
Resilience
Microsoft.Extensions.Http.Resilience— standard retry/circuit-breaker on all HTTP clientsMicrosoft.Extensions.ServiceDiscovery— DNS-based service resolution via Aspire
Testing
- xUnit 2.9 — unit tests per service with Moq and EF InMemory
Aspire.Hosting.Testing— full-stack integration tests spinning up live containers
CI
- GitHub Actions — build, unit tests, and integration tests in separate jobs with NuGet caching
TicketFlow/
├── TicketFlow.AppHost/ # .NET Aspire orchestration (all infra + services)
├── TicketFlow.ServiceDefaults/ # Shared extensions: OTel, health, auth, error handling
├── TicketFlow.Events/ # Shared event contracts (OrderCreatedEvent)
│
├── TicketFlow.Catalog/
│ ├── Protos/catalog.proto
│ ├── Services/CatalogService.cs # gRPC implementation
│ ├── Models/CatalogItem.cs
│ └── Data/
│
├── TicketFlow.Ordering/
│ ├── Controllers/OrdersController.cs
│ ├── Models/Order.cs # Pending → Confirmed → Cancelled
│ └── Data/
│
├── TicketFlow.Identity/
│ ├── Controllers/AuthController.cs
│ ├── Services/TokenService.cs # JWT + refresh token generation
│ ├── Models/ # AppUser, RefreshToken, DTOs
│ └── Data/
│
├── TicketFlow.Gateway/
│ ├── GraphQL/Query.cs # GetConcerts (authorized, via gRPC)
│ ├── GraphQL/Mutation.cs # Login, Register, RefreshToken
│ ├── Services/IdentityService.cs
│ └── Protos/catalog.proto # Client-side gRPC definition
│
├── TicketFlow.Notifications/
│ └── Consumers/OrderCreatedConsumer.cs
│
└── tests/
├── TicketFlow.Identity.Tests/
├── TicketFlow.Ordering.Tests/
├── TicketFlow.Gateway.Tests/
├── TicketFlow.Notifications.Tests/
└── TicketFlow.IntegrationTests/ # Full-stack via Aspire test host
Prerequisites: .NET 10 SDK, Docker Desktop
git clone https://github.com/kacpersmaga/TicketFlow.git
cd TicketFlow
dotnet run --project TicketFlow.AppHostAspire starts all containers and services automatically. The dashboard (typically http://localhost:18888) shows live logs, traces, and resource health for every component.
# Unit tests
dotnet test tests/TicketFlow.Identity.Tests
dotnet test tests/TicketFlow.Ordering.Tests
dotnet test tests/TicketFlow.Gateway.Tests
dotnet test tests/TicketFlow.Notifications.Tests
# Integration tests (requires Docker — boots the full Aspire stack)
dotnet test tests/TicketFlow.IntegrationTestsThe integration test suite uses a shared AppHostFixture (xUnit collection fixture) to boot the entire distributed system once per session, then runs all tests against live HTTP clients. Tests cover auth flows (register → login → refresh → token revocation), order creation, and service health checks.
GitHub Actions runs on every push and pull request to master:
- Build & Unit Tests — restores, builds in Release, runs all unit test projects in parallel with NuGet caching
- Integration Tests — runs after unit tests pass; boots the full Aspire stack on the Ubuntu runner (Docker is available by default) with a 20-minute job timeout