booking-microservices: a .NET 10 flight booking system built on vertical slices
Flight booking system built as .NET 10 microservices with Vertical Slice Architecture, DDD, CQRS, Event Sourcing, gRPC, RabbitMQ, Wolverine, PostgreSQL, MongoDB and .NET Aspire.
At a glance
- What is it?
- booking-microservices is a worked example of a production-style .NET 10 service estate, with four bounded services, PostgreSQL for writes, MongoDB for reads, EventStoreDB for the booking write side, gRPC between services and RabbitMQ through Wolverine for integration events. It is a reference architecture with no released packages, so its value is in the code, not in the binaries.
- Who is it for?
- booking-microservices fits a team learning how a service estate is wired together, or an engineer looking for a working reference for CQRS, the inbox and outbox patterns, gRPC between services and a YARP gateway in C#. It does not fit a team that wants a flight booking product, a reusable library, or a release cadence, because the repository publishes no releases and its last push was on 2026-09-13.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 39 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Four services, four stores, and one public entry point
The system models a flight booking domain split across independently deployable services. The documented service table assigns each one a responsibility and a pair of stores.
Identity owns users, roles and tokens through IdentityServer, with PostgreSQL as its write store and no read store. Flight owns flights, airports, aircraft and seats, writing to PostgreSQL and projecting reads into MongoDB. Passenger owns passenger profiles with the same split. Booking owns the act of booking a seat on a flight for a passenger, and it is the odd one out: its write store is EventStoreDB rather than a relational database. ApiGateway is a single public entry point built on YARP, and the Aspire AppHost provides local orchestration and a dashboard.
The consequence of that table is the design rule for the whole repository. Each service owns its data, so there is no shared schema and no cross-service database joins. A query that needs data from another service cannot read it directly, which is why the request path is split into a synchronous half and an asynchronous half.
The domain itself is deliberately familiar. Flights, seats, passengers and bookings are a bounded problem that most readers already understand, which is the right choice for a repository whose purpose is to show wiring rather than novel business logic.
Commands write, events travel, queries read MongoDB
The read and write paths are physically separated. Commands mutate the write store, which is PostgreSQL or EventStoreDB depending on the service. As part of that mutation, the service publishes integration events through Wolverine's durable outbox to RabbitMQ. Consumers on the other side process those events through a durable inbox and project them into MongoDB read models. Queries then read from MongoDB and nothing else.
The outbox and inbox pair is the mechanism that makes this work without losing events. The outbox pattern gives at-least-once delivery, and the inbox gives idempotent, exactly-once processing on the consumer side. The two together mean a message can be delivered twice without the read model being corrupted.
One part of the flow is deliberately synchronous. Cross-service reads that must complete before a write can proceed, such as Booking validating a flight or a passenger, go over gRPC instead of waiting on the event pipeline. That is a real design decision with a cost: a synchronous gRPC call couples the caller's availability to the callee's, while an event-driven approach would decouple them at the price of not knowing the answer immediately. The repository uses gRPC only where the answer is needed to decide, and events everywhere else.
MediatR carries the commands and queries inside a service, with FluentValidation and logging pipeline behaviours attached, and the CQRS split is enforced at the code level rather than by convention.
A feature folder holds one use case end to end
The code organisation is vertical slice architecture, and the project is specific about what that means in practice. Inside every service, code is grouped by feature rather than by technical layer, with the example given as Flights/Features/CreatingFlight/V1/. Each feature folder holds its endpoint, its command or query, its handler, its validator and any events it raises, so a change to one use case touches only that folder.
This is the structural decision that makes the rest tractable. In a layer-first layout, adding an endpoint means finding the controller folder, then the service folder, then the model folder, and hoping the names line up. With slices, the entire change sits in one directory that a reviewer can read top to bottom.
The top-level layout follows the same logic:
├── src
│ ├── ApiGateway/ # YARP gateway
│ ├── Aspire/ # Aspire AppHost and service defaults
│ ├── BuildingBlocks/ # Shared cross-cutting code (Core, EFCore, Mongo, Wolverine, Jwt, Logging, OpenTelemetry, Polly, TestBase, ...)
│ └── ServicesBuildingBlocks is the counterweight to the slicing rule. It holds the shared cross-cutting code: Core, EFCore, Mongo, Wolverine, Jwt, Logging, OpenTelemetry, Polly and a TestBase library. The interesting judgement is where the line sits. Business logic stays inside a service's feature folders, while the plumbing that every service needs lives once in BuildingBlocks, otherwise each service would reimplement its own retry policy and its own database bootstrap.
Getting the solution to run needs four things on your machine
The documented prerequisites are a .NET 10 SDK, Docker Desktop, and Node.js, the last of which is there for the Husky commit hooks rather than for the application itself. The .NET Aspire CLI and kubectl are optional. The Docker requirement is not a convenience, since the services need PostgreSQL, MongoDB, EventStoreDB and RabbitMQ to exist before any of them can start properly.
The repository has no released packages and no GitHub releases, so there is nothing to install. Everything runs from source. The documentation covers four routes: run with Aspire, run with Docker Compose, run with Kubernetes, and build, run and test manually. Aspire is the intended local path because the AppHost exists precisely to start the dependency graph and show it on a dashboard, and the deployments/docker-compose directory holds both infrastructure-only and full-stack compose files for people who prefer to drive it by hand.
The JavaScript side of the repository exists only for tooling, and its scripts show exactly what it does:
"prepare": "husky && dotnet tool restore",
"format": "dotnet tool run dotnet-csharpier booking-microservices.sln",
"ci-format": "dotnet tool run dotnet-csharpier booking-microservices.sln --check",Husky installs commit hooks, dotnet tool restore brings back the local tool manifest, and CSharpier formats the solution with a separate check variant for CI. The lint step and the formatting step are therefore enforced in the same place, and the commitlint.config.js plus the commitlint dependencies handle message format.
Testing spans unit, integration, end to end and contract
The test strategy is stated as four layers, and each layer maps onto a specific risk. Unit tests use xUnit with NSubstitute for substitution and Bogus for generated data. Integration tests use Testcontainers, which means real PostgreSQL, MongoDB, EventStoreDB and RabbitMQ instances spun up per test run rather than in-memory substitutes. End-to-end tests exercise the assembled system. Contract tests use PactNet, which is what keeps the gRPC and event boundaries honest between services.
The PactNet piece matters most in a system whose services do not share a database. If the Flight service changes the shape of an event that Booking consumes, a contract test is what catches the break, because both sides declare the expected message shape and the test fails when they disagree. In a codebase with shared types, that failure mode does not exist.
The observability stack supports the same goal from the other side. OpenTelemetry traces are collected into Jaeger, metrics are scraped by Prometheus and shown in Grafana, and Serilog with Kibana handles structured logs. The deployments/configs directory holds the collector, Prometheus and Grafana configuration files, so the observability is wired into the deployment rather than left as an exercise.
MIT licensed, no releases, and a branch that keeps moving
The licence is MIT and the LICENSE file sits at the root, so the code can be read, copied and adapted without obligations beyond keeping the notice. The repository lists no GitHub releases at all, which is worth separating from the release history of a mature library: this is a demonstration repository with a continuous integration badge on the main branch, not a package anyone consumes from a feed.
Maintenance is visible in the git history rather than in version numbers. The repository is not archived and the last push was on 2026-09-13. The default branch is main, and the .github directory holds the workflow referenced by the badge at the top of the readme.
The upgrade story for a consumer is therefore blunt. There is no version to pin, no changelog to read and no migration path between releases, because there are no releases. If you want the patterns, you take the current state of main and you own it from there. If you want a supported dependency, this is the wrong repository.
One more practical note sits in the file layout. booking.rest at the root holds REST Client requests for manual API testing, which is a small signal about the intended workflow: a developer starts from the gateway with a prepared set of requests rather than from a script.
Editorial conclusion
booking-microservices fits a team learning how a service estate is wired together, or an engineer looking for a working reference for CQRS, the inbox and outbox patterns, gRPC between services and a YARP gateway in C#. It does not fit a team that wants a flight booking product, a reusable library, or a release cadence, because the repository publishes no releases and its last push was on 2026-09-13. Before adopting its patterns, read src/BuildingBlocks and the deployments/kubernetes manifests, then run the Docker Compose path in deployments/docker-compose to see the services and their data stores start together.
Frequently asked questions
What does booking-microservices demonstrate?
It demonstrates a production-style microservices system end to end, with independent services owning their own databases, asynchronous messaging with durable inbox and outbox patterns, synchronous gRPC calls between services, an API gateway, centralized identity, observability and container and Kubernetes deployment.
Why does the Booking service use EventStoreDB instead of PostgreSQL?
The service table shows EventStoreDB as the write store for the Booking service only, since booking a seat on a flight for a passenger is the write side handled by event sourcing, while Identity, Flight and Passenger write to PostgreSQL.
How does the query path work in booking-microservices?
Commands mutate the write store and publish integration events through Wolverine's durable outbox to RabbitMQ. Consumers process those events through the durable inbox and project them into MongoDB read models, and queries read from MongoDB only.
What do I need to run booking-microservices locally?
The stated prerequisites are the .NET 10 SDK, Docker Desktop and Node.js for the Husky commit hooks, with the .NET Aspire CLI and kubectl optional. The repository has no GitHub releases, so it runs from source using the Aspire AppHost or the compose files in deployments/docker-compose.
How are the gRPC boundaries between services kept correct?
Contract tests with PactNet are part of the test suite alongside unit, integration and end-to-end tests. Integration tests use Testcontainers to run real PostgreSQL, MongoDB, EventStoreDB and RabbitMQ instances rather than in-memory substitutes.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/evangelosvlachos96-dotcom-booking-microservices)Was this useful?