Booking Microservices

A flight-booking system, built the way production microservices are.

Four .NET 10 services — Identity, Flight, Passenger, Booking — each owning its data, talking over gRPC when a call must be synchronous and over RabbitMQ when it need not be. An API gateway in front, OpenTelemetry underneath. One command brings the whole thing up locally.

Architecture diagram: an ingress in front of Identity, Flight, Passenger and Booking services, each with Postgres or EventStore plus Mongo; a message processor with inbox, outbox and internal command processors; RabbitMQ, Redis and an OpenTelemetry collector feeding Elastic, Kibana, Loki, Prometheus, Grafana, Jaeger, Zipkin and Tempo

Start in three steps

You need the .NET 10 SDK and Docker Desktop. Everything else runs in containers.

1 · Trust a dev certificate

So the containers can serve TLS: dotnet dev-certs https -ep ~/.aspnet/https/aspnetapp.pfx -p password, then dotnet dev-certs https --trust.

2 · Run with Aspire

aspire run starts every service and its infrastructure. The dashboard with logs, traces and metrics is at localhost:18888.

3 · Call the API

Every service serves OpenAPI at /swagger and /scalar/v1. Open booking.rest in VS Code's REST Client to authenticate as van1 and walk through a booking.

Prefer Docker Compose or Kubernetes? Both live in deployments/:

docker-compose -f ./deployments/docker-compose/docker-compose.infrastructure.yaml up -d   # infrastructure only
docker-compose -f ./deployments/docker-compose/docker-compose.yaml up -d                  # full stack

kubectl apply -f ./deployments/kubernetes/booking-cert-manager.yml
kubectl apply -f ./deployments/kubernetes/booking-microservices.yml

How it works

Each service is small on the outside and organised by feature on the inside.

One folder per use case

Code is grouped by feature, not by layer. Flights/Features/CreatingFlight/V1/ holds the endpoint, the command, its handler, the validator and the events it raises. A change to one use case touches one folder. MediatR runs a pipeline with validation and logging behaviours around every request.

Vertical slice architecture diagram: each request cuts through API, application and domain as one self-contained slice instead of crossing horizontal layers

Write here, read there

Commands change the write store: PostgreSQL for Flight, Passenger and Identity, EventStoreDB for Booking, which is fully event-sourced. Consumers project the resulting events into MongoDB read models. Queries only ever read MongoDB.

Messages that do not get lost

Integration events leave through Wolverine's durable outbox to RabbitMQ (at-least-once delivery) and arrive through a durable inbox (idempotent, exactly-once processing). When Booking must confirm a flight or a passenger right now, it calls that service over gRPC instead.

One door in

YARP is the public entry point and routes to the services. Duende IdentityServer issues OpenID Connect / OAuth2 tokens; the seeded users van1 (admin) and van2 (user) are enough to try every endpoint.

Tested at every level

Unit tests with NSubstitute, integration and end-to-end tests that start PostgreSQL, MongoDB and RabbitMQ through Testcontainers, and consumer-driven contract tests with PactNet. dotnet test runs them all; Docker must be running.

The services

Every service owns its data and exposes a small REST surface through minimal APIs.

ServiceResponsibilityWrite storeRead store
IdentityUsers, roles, tokens (IdentityServer)PostgreSQL
FlightFlights, airports, aircraft, seatsPostgreSQLMongoDB
PassengerPassenger profilesPostgreSQLMongoDB
BookingBooking a seat on a flight for a passengerEventStoreDBMongoDB
ApiGatewaySingle public entry point (YARP)
Aspire AppHostLocal orchestration and dashboard
Observability and hygiene out of the box

Traces, metrics and logs leave every service through OpenTelemetry to a collector, then on to Jaeger, Prometheus, Grafana and Serilog with Kibana. Polly handles retries and circuit breaking; ASP.NET Core health checks report readiness. CSharpier, Husky and commitlint enforce formatting and Conventional Commits before each commit.