SE 4458 — Software Architecture & Design of Modern Large Scale Systems — Final Project (May 2026) Group 1
A production-grade, Hotels.com-like hotel booking platform built as a cloud-native microservices system on C# .NET Core 8. The system covers the full booking lifecycle: hotel inventory management, availability search with Redis caching, optimistic-concurrency-safe room booking, AI-powered conversational search, per-category hotel reviews in MongoDB, and an asynchronous notification pipeline over RabbitMQ.
- Deliverable Links
- Demo Video
- Technology Stack
- API Reference
- Services Overview
- Cloud Infrastructure & Deployment Map
- Architecture
- Architectural Decisions & Design Patterns
- Database Design
- Project Requirements & Assumptions
- Issues Encountered & Engineering Solutions
- Running Locally
- Repository Structure
All backend services are deployed to Azure App Service Linux (Italy North, B1 plan). The frontend is deployed to Vercel.
| Concern | Technology | Notes |
|---|---|---|
| Backend | C# .NET Core 8 Web API | All 5 microservices |
| API Gateway | Ocelot + MMLib.SwaggerForOcelot | Single entry point; JWT validation + rate limiting |
| Auth / IAM | Supabase Auth (JWT Bearer, OIDC, RS256) | No local auth — IAM-only per spec |
| SQL Database | Supabase PostgreSQL (Npgsql) | Session pooler for IPv4 routing |
| NoSQL Database | MongoDB Atlas | Comments Service only |
| Distributed Cache | Upstash Redis | Search results (15 min TTL) + hotel detail (60 min TTL) |
| Message Broker | CloudAMQP RabbitMQ (SSL, port 5671) | hotel.reservations exchange, reservation.created queue |
| Nightly Scheduler | Azure Logic App | Triggers POST /api/v1/notifications/capacity-check nightly |
| AI Model | Google Gemini 1.5 Flash | Via IAiProvider abstraction; swappable without business logic changes |
| ORM | EF Core 8 Code-First | Separate DbContext per bounded context; EF Core migrations |
| Logging | Serilog + Correlation ID enrichment | Distributed tracing across services |
| Testing | xUnit | Unit tests on business logic; 4 test projects |
| CI/CD | GitHub Actions | Build → test → Docker build verify on every push to main |
| Containerization | Docker (Dockerfile per service) | Verified in CI; no image file committed |
| Frontend | React + Vite | SPA with Supabase Auth SDK + Leaflet map |
| Frontend Hosting | Vercel | Auto-deploy on git push |
| Resilience | Polly | Circuit breakers; 503 on SQL unreachable |
All requests from the frontend go through the API Gateway at:
https://hotel-gateway-axhpduheewacbvhc.italynorth-01.azurewebsites.net/gateway/v1/
| Method | Gateway Path | Downstream | Auth | Description |
|---|---|---|---|---|
GET |
/gateway/v1/search/hotels |
HotelService | Public | Search hotels by destination, dates, guest count. Returns paginated results with lat/lng for map. Rate-limited: 100 req/min. |
GET |
/gateway/v1/hotels/{everything} |
HotelService | Public | Hotel detail + room types + room detail with xmin token |
GET |
/gateway/v1/bookings |
HotelService | Bearer JWT | Get all bookings for the authenticated user |
POST |
/gateway/v1/bookings |
HotelService | Bearer JWT | Create a booking (requires rowVersion for optimistic concurrency) |
DELETE |
/gateway/v1/bookings/{bookingId} |
HotelService | Bearer JWT | Cancel a booking |
GET |
/gateway/v1/admin/hotels |
HotelService | Bearer JWT (Admin) | List all hotels (admin view) |
POST |
/gateway/v1/admin/hotels |
HotelService | Bearer JWT (Admin) | Create a new hotel |
GET/PUT/DELETE |
/gateway/v1/admin/hotels/{everything} |
HotelService | Bearer JWT (Admin) | Update hotel, manage room types, delete hotel |
POST |
/gateway/v1/admin/inventory |
HotelService | Bearer JWT (Admin) | Upsert inventory block (start/end date, room count, availability flag) |
| Method | Gateway Path | Downstream | Auth | Description |
|---|---|---|---|---|
GET |
/gateway/v1/comments/{hotelId} |
CommentsService | Public | Get paginated comments + category score breakdown |
POST |
/gateway/v1/comments/{hotelId} |
CommentsService | Bearer JWT | Submit a review (authenticated assumption — see §10) |
| Method | Gateway Path | Downstream | Auth | Description |
|---|---|---|---|---|
GET |
/gateway/v1/notifications |
NotificationService | Bearer JWT (Admin) | Get all low-capacity alerts (paginated) |
PATCH |
/gateway/v1/notifications/{id}/read |
NotificationService | Bearer JWT | Mark an alert as read |
POST |
/gateway/v1/notifications/capacity-check |
NotificationService | None | Nightly cron trigger (called by Azure Logic App) |
| Method | Gateway Path | Downstream | Auth | Description |
|---|---|---|---|---|
POST |
/gateway/v1/ai/chat |
AiAgentService | Bearer JWT | Stateless conversational search & booking. Rate-limited: 30 req/min. |
Search Hotels
GET /gateway/v1/search/hotels?destination=Bodrum&startDate=2026-06-01&endDate=2026-06-05&guestCount=2&page=1&pageSize=10
[
{
"hotelId": "3fa85f64-...",
"name": "Hyde Bodrum",
"destination": "Bodrum",
"latitude": 37.034,
"longitude": 27.43,
"pricePerNight": 9305.8,
"availableRooms": 10,
"rating": 9.6,
"totalReviews": 163,
"roomTypeId": "room-type-uuid"
}
]Authenticated users receive a 15% discounted
pricePerNight. Base prices are cached in Redis; discount is applied at response time and never stored.
Create Booking
POST /gateway/v1/bookings
Authorization: Bearer <jwt>
{
"hotelId": "3fa85f64-...",
"roomTypeId": "room-type-uuid",
"checkInDate": "2026-06-01",
"checkOutDate": "2026-06-05",
"guestCount": 2,
"rowVersion": 1234567
}Returns
409 ConflictifrowVersionis stale (another user booked the same slot concurrently).
AI Chat
POST /gateway/v1/ai/chat
Authorization: Bearer <jwt>
{
"message": "I'd like to book a hotel in Bodrum from June 1 to June 5 for 2 adults.",
"messages": [...],
"contextState": {}
}The largest service — owns three functional areas (one codebase, one deployment):
- Admin — authenticated CRUD for hotels, room types, and inventory blocks. Invalidates Redis cache on inventory update.
- Search — public endpoint with cache-aside pattern (Redis → SQL fallback). Returns coordinates for map. Applies 15% discount at response time for JWT holders.
- Booking — authenticated booking with optimistic concurrency via PostgreSQL
xmin. PublishesReservationCreatedEventto RabbitMQ on success. Supports viewing and cancelling bookings.
DbContexts: CatalogDbContext (Hotels, RoomTypes, InventoryBlocks) + BookingDbContext (Bookings) — both on Supabase PostgreSQL, separate EF Core migrations.
Stores hotel reviews in MongoDB Atlas. One document per hotel contains all reviews and pre-aggregated category scores (cleanliness, staff, facilities, location, eco-friendliness). GET is public; POST requires Bearer JWT.
Two responsibilities in one deployment:
- Always-on RabbitMQ consumer —
IHostedServicesubscribes toreservation.createdqueue. ACK on success; NACK+requeue on failure. Logs reservation details to console (simulated notification). - Nightly cron endpoint —
POST /api/v1/notifications/capacity-checkcalled by Azure Logic App. Queries HotelService's API for inventory blocks below 20% capacity in the next 30 days. Clears and repopulatesNotificationAlertstable in Supabase PostgreSQL (owns its ownNotificationsDbContext).
Stateless Gemini 1.5 Flash–powered conversational agent. Implements a two-step booking flow: parse intent → clarify if needed → present hotel options → confirm → book. The IAiProvider interface decouples business logic from the Gemini SDK — switching to OpenAI/Claude/Azure OpenAI requires only a new class + DI swap.
Single entry point for all frontend traffic. Responsibilities:
- Routing — 14 routes forwarding to 4 downstream services
- JWT Authentication — validates Supabase-issued Bearer tokens on protected routes before forwarding
- Rate limiting — 100 req/min on search, 30 req/min on AI chat
- Swagger aggregation — MMLib.SwaggerForOcelot aggregates Swagger from all downstream services
┌─────────────────────────────────────────────────────────────────────┐
│ Azure Italy North │
│ │
│ ┌──────────────┐ ┌────────────────────────────────────────┐ │
│ │ Vercel │ │ Azure App Services (B1) │ │
│ │ (Frontend) │ │ │ │
│ │ React+Vite │───▶│ Ocelot API Gateway │ │
│ └──────────────┘ │ hotel-gateway-axhpduheewacbvhc │ │
│ │ │ │ │
│ │ ┌───────────┼───────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ HotelSvc CommentsS AiAgentSvc │ │
│ │ NotifySvc │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Data Layer │ │
│ │ Supabase PostgreSQL │ MongoDB Atlas │ Upstash Redis │ │
│ │ (HotelSvc + NotifySvc│ (CommentsService│ (hotel search + │ │
│ │ CatalogDb+BookingDb │ hotelReviews │ hotel detail) │ │
│ │ NotificationAlerts) │ collection) │ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Async & Scheduling │ │
│ │ CloudAMQP RabbitMQ (SSL) │ Azure Logic App (nightly) │ │
│ │ hotel.reservations exch. │ → POST /notifications/ │ │
│ │ reservation.created queue │ capacity-check │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────┐ ┌────────────────┐ │
│ │ Supabase Auth (IAM) │ │ Google Gemini │ │
│ │ JWT issuer (RS256) │ │ 1.5 Flash API │ │
│ └───────────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Deployment procedure: Services are deployed manually via Kudu's /api/zipdeploy REST API (not az webapp deploy — see §11). CI/CD builds and tests on every push but does not auto-deploy to Azure. Frontend auto-deploys to Vercel on every git push.
Browser / Mobile
│
│ HTTPS
▼
┌─────────────────────────┐
│ API Gateway (Ocelot) │ ← JWT validation, rate limiting, routing
└────────────┬────────────┘
│
┌──────────┼──────────────────┐
│ │ │
▼ ▼ ▼
HotelService CommentsService AiAgentService
│ │ │ (MongoDB) (Gemini API)
│ │ │
│ │ └── BookingDbContext (Bookings table)
│ └───── CatalogDbContext (Hotels, RoomTypes, InventoryBlocks)
│ + Redis cache-aside
│
│ on booking success
└──[AMQP publish]──▶ RabbitMQ ──▶ NotificationService
│
├── Log reservation (always-on consumer)
└── NotificationsDbContext (NotificationAlerts table)
↑
Azure Logic App (nightly trigger)
Each service is independently deployed, independently versioned, and owns its own data store. Services never share a DbContext, never query another service's database, and never use hard SQL FK constraints across service boundaries. Cross-service data references are soft references (plain GUIDs).
Request → API Gateway
│
├── [AuthenticationOptions: Bearer] validates JWT signature (Supabase JWKS)
│ → Rejects with 401 if token is invalid or missing on protected routes
│
└── Forwards request + original Authorization header downstream
│
▼
HotelService / NotificationService
│
└── IsAdmin(): decodes app_metadata.roles from JWT
→ Rejects with 403 if role ≠ admin
The gateway handles authentication (is the token valid?). Downstream services handle authorization (is the user allowed to do this?). Ocelot forwards the Authorization header unchanged — it does not strip it after validation.
| Pattern | Service | Implementation | Why |
|---|---|---|---|
| Cache-Aside | HotelService | SearchService.cs checks Redis → SQL on miss → repopulates Redis |
Search must return < 500ms; Redis makes this possible without hitting Supabase on every request |
| Optimistic Concurrency | HotelService | PostgreSQL xmin system column as EF Core concurrency token; DbUpdateConcurrencyException → HTTP 409 |
Prevents overbooking without pessimistic row locking |
| Strategy | HotelService | IPricingStrategy → DiscountPricingStrategy (15% for JWT holders) vs BasePricingStrategy |
Swappable pricing tiers without modifying core booking logic |
| Singleton | All services | IConnectionMultiplexer (Redis), IConnection (RabbitMQ) registered as singletons |
One connection pool per service instance; prevents connection exhaustion |
| Factory Method | NotificationService | INotificationFactory creates BookingNotification vs LowCapacityNotification |
Decouples alert creation from the consumer/scheduler decision logic |
| Facade | AiAgentService | HotelServiceClient wraps all internal REST calls to HotelService |
AI business logic never knows it's calling a remote service |
| Provider Abstraction | AiAgentService | IAiProvider interface → GeminiAiProvider |
Switching AI models requires only a new class + DI swap; zero business logic changes |
| Database-per-Service | All services | Separate EF Core DbContext per bounded context; no shared DB |
Core microservices principle; enables independent scaling and deployment |
| Event-Driven (Pub/Sub) | HotelService → NotificationService | ReservationCreatedEvent published to RabbitMQ on booking success |
Decouples booking from notification delivery; notification failures cannot roll back bookings |
1. Single HotelService (Admin + Search + Booking merged) The project spec's deployment diagram shows one "Hotel Service" box responsible for all hotel operations. These three functional areas share the same Supabase schema (Hotels, RoomTypes, InventoryBlocks, Bookings), making a single deployment more practical. Internal separation is maintained via separate DbContexts and service classes.
2. Supabase PostgreSQL instead of Azure SQL Server
Supabase provides managed PostgreSQL with built-in Auth (JWT/OIDC) in one platform. This eliminated a separate identity service. The session pooler endpoint (aws-1-eu-central-1.pooler.supabase.com) is used to route over IPv4, avoiding direct connection IPv6 failures in cloud environments.
3. PostgreSQL xmin instead of SQL Server ROWVERSION
Since the project uses PostgreSQL instead of the spec's assumed SQL Server, EF Core's IsRowVersion() (which maps to ROWVERSION/timestamp) is not available. The equivalent is PostgreSQL's built-in xmin system column, configured as a shadow EF Core concurrency token via UseXminAsConcurrencyToken().
4. Never Cache Discounted Prices Redis stores base prices only. The 15% discount is computed at response time based on JWT presence. This prevents a discounted price being served to an unauthenticated user on a cache hit, and makes the discount policy independently changeable without cache invalidation.
5. Cache Key Versioning (v2: prefix)
When a data structure fix was deployed (duplicate hotel deduplication), existing cached entries had an incompatible structure. Rather than flushing all Redis keys manually, a v2: prefix was added to the search cache key pattern. This instantly invalidated all old entries by making them unreachable, with zero downtime.
6. NotificationsDbContext with Raw SQL Table Creation
EF Core's EnsureCreated() silently skips table creation when the target database already has any tables. Since Supabase PostgreSQL is shared across services (multiple tables already exist), EnsureCreated() would never create the NotificationAlerts table. Solution: raw CREATE TABLE IF NOT EXISTS SQL on service startup wrapped in a try-catch, making the service self-initializing on first deploy.
| Context | Technology | Service | Tables / Collections |
|---|---|---|---|
CatalogDbContext |
Supabase PostgreSQL | HotelService | Hotels, RoomTypes, InventoryBlocks |
BookingDbContext |
Supabase PostgreSQL | HotelService | Bookings |
NotificationsDbContext |
Supabase PostgreSQL | NotificationService | NotificationAlerts |
CommentsDbContext |
MongoDB Atlas | CommentsService | hotelReviews collection |
| Redis | Upstash Redis | HotelService | Search results cache, hotel detail cache |
erDiagram
%% ==========================================
%% CONTEXT A: Hotel Catalog & Inventory
%% Owned by: Hotel Service (CatalogDbContext)
%% ==========================================
HOTELS ||--o{ ROOM_TYPES : "has"
ROOM_TYPES ||--o{ INVENTORY_BLOCKS : "allocates"
HOTELS {
uuid HotelId PK
varchar Name
varchar Destination "indexed"
decimal Latitude
decimal Longitude
decimal BaseRating "nullable"
int TotalReviews
varchar ImageUrl "nullable"
text Description "nullable"
bool IsActive
}
ROOM_TYPES {
uuid RoomTypeId PK
uuid HotelId FK
varchar TypeName
int MaxGuests
decimal BasePricePerNight
}
INVENTORY_BLOCKS {
uuid InventoryId PK
uuid RoomTypeId FK
date StartDate "indexed"
date EndDate "indexed"
int TotalCount
int AvailableCount
bool IsAvailable
xid xmin "PostgreSQL system column - optimistic concurrency token"
}
%% ==========================================
%% CONTEXT B: Reservations
%% Owned by: Hotel Service (BookingDbContext)
%% ==========================================
BOOKINGS {
uuid BookingId PK
varchar UserId "JWT sub claim - no FK"
uuid HotelId "soft ref - no FK"
uuid RoomTypeId "soft ref - no FK"
date CheckInDate
date CheckOutDate
int GuestCount
decimal TotalAmount
varchar Status
timestamptz CreatedAt
}
%% ==========================================
%% CONTEXT C: Notification Alerts
%% Owned by: Notification Service (NotificationsDbContext)
%% ==========================================
NOTIFICATION_ALERTS {
uuid NotificationId PK
uuid HotelId "soft ref - no FK"
varchar HotelName
varchar RoomTypeName
int AvailableCount
int TotalCount
float CapacityRatio
date StartDate
date EndDate
timestamptz CreatedAt
bool IsRead
}
| Key Pattern | TTL | Value | Notes |
|---|---|---|---|
v2:search:{destination}:{startDate}:{endDate}:{guestCount} |
15 min | JSON array of hotel results | Base prices only. v2: prefix busts pre-fix cache entries. |
hotel:detail:{hotelId} |
60 min | JSON hotel detail object | Explicitly evicted on admin inventory update. |
{
"hotelId": "3fa85f64-...",
"totalReviews": 163,
"overallScore": 9.2,
"categoryScores": {
"cleanliness": 9.6,
"staff": 9.6,
"facilities": 9.4,
"locationCondition": 9.6,
"ecoFriendly": 9.4
},
"reviews": [
{
"reviewId": "rev-uuid-001",
"author": "Simge",
"tripType": "4 gecelik seyahat",
"rating": 8.0,
"text": "Great location and very clean.",
"date": "2025-06-16T00:00:00Z",
"hotelReply": {
"repliedBy": "Enver",
"replyText": "Geri bildiriminiz için teşekkür ederiz.",
"replyDate": "2025-06-16T00:00:00Z"
}
}
]
}1. GET /hotels/{id}/rooms/{roomTypeId}
→ Server returns room detail + xmin value (PostgreSQL transaction ID)
→ Client stores xmin as rowVersion
2. POST /bookings { ..., rowVersion: <xmin> }
→ EF Core executes:
UPDATE "InventoryBlocks"
SET "AvailableCount" = "AvailableCount" - 1
WHERE "InventoryId" = @id
AND xmin = @rowVersion ← fails if another user booked first
AND "AvailableCount" > 0
3a. 1 row affected → booking confirmed → 201 Created
3b. 0 rows affected → DbUpdateConcurrencyException → 409 Conflict (try again)
The system ships with seed data for meaningful end-to-end testing:
- 8 hotels across Istanbul (×2), Izmir (×1), Bodrum (×2), Antalya (×3) — all with valid lat/lng for the map feature
- 2–3 room types per hotel (Standard, Family, Suite variants)
- 3 inventory blocks per room type covering the next 90 days, with at least one block at < 20% capacity to trigger the nightly alert
| Requirement | Status | Notes |
|---|---|---|
| Hotel Admin Service (auth-gated) | ✅ | CRUD hotels, room types, inventory — JWT + IsAdmin() check |
| Hotel Search (destination/dates/guests) | ✅ | Cached, paginated, returns lat/lng |
| "Haritada göster" map | ✅ | Leaflet/OpenStreetMap toggle on search results |
| 15% discount for logged-in users | ✅ | Strategy pattern; never cached |
| Book Hotel (capacity decrement, no payment) | ✅ | Optimistic concurrency; 409 on overbooking |
| Hotel Comments (category graph + paginated) | ✅ | MongoDB, 5 category scores, paginated |
| Notification — nightly capacity check (<20%) | ✅ | Azure Logic App → capacity-check endpoint → NotificationAlerts |
| Notification — RabbitMQ reservation events | ✅ | Always-on IHostedService consumer |
| AI Agent (conversational search + booking) | ✅ | Gemini 1.5 Flash; two-step confirm flow |
| API Gateway (all APIs via gateway) | ✅ | Ocelot; 14 routes |
| IAM (no local auth) | ✅ | Supabase Auth only |
| Redis distributed cache | ✅ | Upstash Redis |
| NoSQL for comments | ✅ | MongoDB Atlas |
| Dockerfiles (no image file) | ✅ | One per service (6 total including frontend) |
| Separate deployments (5 services) | ✅ | Azure App Service Linux, Italy North |
REST versioning (/v1/) |
✅ | All routes prefixed |
| Pagination | ✅ | Search, comments, notifications |
| Health endpoints | ✅ | /health on all services |
| Swagger per service | ✅ | Individual + aggregated on gateway |
| xUnit tests | ✅ | HotelService, CommentsService, NotificationService, AiAgentService |
| Serilog + Correlation IDs | ✅ | Distributed tracing |
| GitHub Actions CI | ✅ | Build + test + docker build verify |
| README with deployed URLs, ER, assumptions, issues, video | ✅ | This document |
Assumption 1 — Booking and AI Agent require JWT authentication; search is public
The project document specifies that logged-in users will receive a 15% discount, implying that hotel search should be accessible to unauthenticated users. Therefore, searching for hotels is open to the public. However, the following features require the user to create an account and log in via the IAM service (Supabase Auth):
- Book Hotel — restricted to authenticated users to prevent spam reservations, safely decrement capacity, and track the reservation owner via the JWT
subclaim.- AI Agent chat — restricted to authenticated users because the agent can initiate bookings on the user's behalf. Allowing anonymous users to trigger bookings through the AI would bypass the same anti-spam and capacity-safety guarantees that protect the direct booking flow.
Assumption 2 — POST /comments requires JWT authentication
The project spec only shows the comments display UI. A POST /api/v1/comments/{hotelId} endpoint was added because:
- Without it, the NoSQL database can only contain static seeded data — there is no mechanism for users to submit reviews dynamically.
- The PDF mockup shows "verified" labels and stay-duration metadata (e.g., "4 gecelik seyahat") on reviews, implying only authenticated, verified guests can post comments. The endpoint validates the Bearer JWT before accepting any submission.
Assumption 3 — RabbitMQ consumer is always-on, not nightly batch
The project PDF groups both notification tasks under "write a nightly scheduled task", which could imply a nightly batch pull from the queue. The queue consumer is implemented as an always-on IHostedService (ACK/NACK per message) rather than a nightly batch job. A batch-pull pattern would leave booking confirmation messages undelivered for up to 24 hours — contradicting the real-time notification intent of a message queue.
Assumption 4 — Notification alerts are in-website, not email/SMS
Low-capacity alerts are delivered as in-website notifications stored in the NotificationAlerts table. This provides a persistent, queryable audit trail the admin panel can display in real time with a 60-second refresh cycle. Email/SMS integration would require a third-party service (SendGrid, Twilio) and was not specified in the project definition.
Assumption 5 — Sign-Up page is implemented
The project mockups do not show a self-registration screen. A Sign-Up page was implemented to make the 15% member discount flow fully demonstrable end-to-end. Without self-registration, a test evaluator cannot obtain a JWT and the discount path cannot be exercised during the demo. Account creation goes directly to Supabase Auth — no custom auth code exists in the backend.
Assumption 6 — Admin and user client are a single React app
The project diagram shows a separate "Admin Client" and "Client". In the implementation, both are a single React SPA with role-based routing: users with app_metadata.roles = admin see the Admin panel; regular users see search/booking/comments. This simplifies deployment without reducing functionality.
Assumption 7 — Two-layer auth model (Gateway + Service)
The API Gateway validates JWT authenticity (authentication — is the token valid and unexpired?). HotelService's IsAdmin() checks app_metadata.roles in the forwarded JWT (authorization — is the user an admin?). Both layers are active on admin routes. Ocelot forwards the Authorization: Bearer header to downstream services unchanged — it does not strip it after gateway-level validation.
Assumption 8 — Notification search vs. capacity alert apparent contradiction
The search page may show 10 available rooms for a hotel while the Notifications panel shows the same hotel as low-capacity. This is correct, not a bug. The seed data creates 3 inventory blocks per room type:
- Block 1 (near-term): 10 rooms available → what search hits for current dates
- Block 2 (mid-term, June): 1 of 10 rooms available → 10% capacity → triggers the < 20% alert
- Block 3 (summer): 8 of 10 rooms available
The nightly scheduler looks 30 days ahead for any block below 20%. A hotel can be fully available today but nearly full for a future period. The alert card shows the specific date range at risk.
Symptom: The Supabase direct connection URL (db.xxx.supabase.co:5432) fails in GitHub Actions CI and on macOS with "No route to host."
Root cause: Supabase's direct connection endpoint resolves to an IPv6 address. GitHub Actions runners and some macOS networking paths cannot route to IPv6 hosts.
Fix: Use Supabase's session pooler endpoint (aws-1-eu-central-1.pooler.supabase.com:5432?sslmode=require), which routes over IPv4 and also provides connection pooling.
Symptom: CI pipeline health checks block for 5 full seconds when Redis is unreachable (no credentials configured in CI).
Root cause: StackExchange.Redis defaults to ConnectTimeout = 5000ms and SyncTimeout = 5000ms. A PingAsync() call on a disconnected multiplexer waits the full timeout.
Fix: Set ConnectTimeout = 1000, SyncTimeout = 2000 in the Redis connection string configuration, reducing the worst-case CI block from 5s to 2s.
Symptom: Comments Service health check hangs for 30 seconds in CI before failing.
Root cause: MongoDB driver's default ServerSelectionTimeout is 30 seconds. Without Atlas credentials in CI, every connection attempt waits the full 30s.
Fix: Set ServerSelectionTimeout = TimeSpan.FromSeconds(3) in MongoClientSettings on startup.
Symptom: RabbitMQ connection refused on default port 5672.
Root cause: CloudAMQP uses a custom virtual host (matches username) and requires SSL on port 5671. The default ConnectionFactory with no SSL settings is rejected.
Fix: Explicitly set VirtualHost, Port = 5671, and SslOption { Enabled = true, ServerName = host } in all three usages (Publisher, Consumer, HealthCheck).
Symptom: App Service deployment to West Europe fails for the student subscription.
Root cause: Azure student subscriptions have region restrictions.
Fix: All 5 services deployed to Italy North (italynorth-01.azurewebsites.net).
Symptom: Standard Azure CLI deploy command fails with ResourceNotFound or 400.
Root cause: az webapp deploy uses the management.azure.com token scope internally. Kudu's /api/zipdeploy endpoint requires the management.core.windows.net scope. Token scope mismatch causes 400.
Fix: Use Kudu's REST API directly with the correct token:
TOKEN=$(az account get-access-token --resource "https://management.core.windows.net/" --query accessToken -o tsv)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/zip" \
--data-binary @app.zip \
"https://<scm-host>/api/zipdeploy?isAsync=true"Symptom: Deployment status fails; logs show "Extract zip" error.
Root cause: dotnet publish -c Release on macOS (Apple Silicon) produces a native macOS ARM64 launcher binary with no file extension (e.g., HotelService, ApiGateway). Kudu's Linux zip extractor rejects zips containing a file matching the project name with no extension.
Fix: Always exclude the launcher when building the deployment zip:
skip = {'HotelService'} # {'ApiGateway'} for the gateway, etc.
with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as z:
[z.write(os.path.join(src, f), f) for f in os.listdir(src) if f not in skip]Symptom: Deploy commands appear to succeed (exit 0) but the app is never updated.
Root cause: Commands piped through | tail -5 make the shell expression's exit code equal to tail's (always 0), masking the real failure from the deploy command.
Fix: Capture the deploy command's output separately before piping, or check the Kudu async deployment status via the Location header from the 202 response.
Symptom: App starts but all connections fail; logs show placeholder values.
Root cause: appsettings.json (committed to git) has placeholder connection strings. Real credentials are in appsettings.Development.json (gitignored). Without ASPNETCORE_ENVIRONMENT=Development in Azure App Settings, only the placeholder file loads.
Fix:
az webapp config appsettings set --name <app> --resource-group hotel-booking-rg \
--settings ASPNETCORE_ENVIRONMENT=DevelopmentSymptom: Deploy shows status 4 (success), new DLL is verified on disk in /home/site/wwwroot/, but Swagger doesn't show new routes and old behavior persists.
Root cause: The app was originally deployed via a mechanism that placed files in /home/site/wwwroot/HotelService/ (a subdirectory). The App Service startup command was set to dotnet /home/site/wwwroot/HotelService/HotelService.dll. All subsequent Kudu zip deploys write to /home/site/wwwroot/ directly — a different path. The running process loaded the old subdirectory DLL, which was never updated.
Discovery: Read the startup command via az webapp config show ... --query appCommandLine. Verified the running DLL path via Kudu's command API. Compared md5sum of the file the process was loading vs. the newly deployed one.
Real-world impact: The gateway's old ocelot.json was missing the DELETE method on admin hotel routes. Hotel deletes from the frontend reached the gateway, which silently dropped them (no matching Ocelot route → no forwarding). Frontend received no error. Only discovered by reading the running ocelot.json from the subdirectory via Kudu command API.
Fix (one-time, applied to HotelService and ApiGateway):
az webapp config set --name hotel-hotelservice --resource-group hotel-booking-rg \
--startup-file "dotnet /home/site/wwwroot/HotelService.dll"
az webapp config set --name hotel-gateway --resource-group hotel-booking-rg \
--startup-file "dotnet /home/site/wwwroot/ApiGateway.dll"Symptom: dotnet publish fails with NETSDK1022: Duplicate 'Content' items were included for ocelot.json.
Root cause: The SDK includes ocelot.json implicitly as a Content item. An explicit <Content Include="ocelot.json"> in the .csproj file created a duplicate.
Fix: Changed <Content Include="ocelot.json"> to <Content Update="ocelot.json"> to override the implicit entry without duplicating it.
Symptom: NotificationAlerts table never gets created; Notification Service crashes on first query with "relation does not exist."
Root cause: EF Core's EnsureCreated() skips all table creation if the target database has any tables already. The shared Supabase database has many tables from HotelService migrations, so EnsureCreated() does nothing.
Fix: Raw SQL on startup:
CREATE TABLE IF NOT EXISTS "NotificationAlerts" (
"NotificationId" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
...
);Wrapped in a try-catch so startup failures are logged but don't crash the service.
- .NET 8 SDK
- Node.js 20+
- Access to: Supabase project, MongoDB Atlas cluster, Redis instance, CloudAMQP RabbitMQ, Gemini API key, Supabase Auth
Each backend service reads secrets from appsettings.Development.json (gitignored). Copy appsettings.json as a starting point:
# Example: HotelService
cp backend/HotelService/appsettings.json backend/HotelService/appsettings.Development.jsonFill in the following in each service's appsettings.Development.json:
| Service | Keys Required |
|---|---|
| HotelService | ConnectionStrings:CatalogDb, ConnectionStrings:BookingDb, Redis:ConnectionString, Jwt:Authority, Jwt:Audience, RabbitMQ:* |
| CommentsService | MongoDb:ConnectionString, MongoDb:DatabaseName, Jwt:Authority, Jwt:Audience |
| NotificationService | ConnectionStrings:NotificationsDb, RabbitMQ:*, HotelServiceUrl |
| AiAgentService | AI:ApiKey, AI:ModelName, HotelServiceUrl, Jwt:Authority, Jwt:Audience |
| ApiGateway | Jwt:Authority, Jwt:Audience |
Start services in any order (gateway last):
cd backend/HotelService && dotnet run
cd backend/CommentsService && dotnet run
cd backend/NotificationService && dotnet run
cd backend/AiAgentService && dotnet run
cd backend/ApiGateway && dotnet runDefault ports (configurable in launchSettings.json):
- HotelService:
http://localhost:5001 - CommentsService:
http://localhost:5002 - NotificationService:
http://localhost:5003 - AiAgentService:
http://localhost:5004 - ApiGateway:
http://localhost:5158
For local runs, update ocelot.json downstream hosts to localhost with the appropriate ports.
cd frontend
cp .env.example .env
# Edit .env: set VITE_GATEWAY_URL=http://localhost:5158
# set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY from your Supabase project
npm install
npm run devFrontend runs at http://localhost:5173.
# All test projects
dotnet test backend/HotelService.Tests/HotelService.Tests.csproj
dotnet test backend/CommentsService.Tests/CommentsService.Tests.csproj
dotnet test backend/NotificationService.Tests/NotificationService.Tests.csproj
dotnet test backend/AiAgentService.Tests/AiAgentService.Tests.csproj# From the backend/ directory (shared SharedKernel is in build context)
docker build -f HotelService/Dockerfile . -t hotelbooking/hotelservice
docker build -f ApiGateway/Dockerfile . -t hotelbooking/gateway
# Frontend
docker build -f frontend/Dockerfile frontend/ -t hotelbooking/frontendAll services expose /health. A healthy service returns HTTP 200:
curl https://hotel-gateway-axhpduheewacbvhc.italynorth-01.azurewebsites.net/health
curl https://hotel-hotelservice-aje8f7f7dqb5f0a5.italynorth-01.azurewebsites.net/healthHotelBookingSystem/
├── backend/
│ ├── SharedKernel/ # Shared events, DTOs (ReservationCreatedEvent)
│ ├── ApiGateway/ # Ocelot gateway + ocelot.json route config
│ ├── HotelService/ # Admin + Search + Booking (CatalogDbContext, BookingDbContext)
│ │ └── Migrations/
│ │ ├── Catalog/ # EF Core migrations for CatalogDbContext
│ │ └── Booking/ # EF Core migrations for BookingDbContext
│ ├── CommentsService/ # MongoDB reviews (CommentsDbContext)
│ ├── NotificationService/ # RabbitMQ consumer + nightly cron (NotificationsDbContext)
│ ├── AiAgentService/ # Gemini AI chat (IAiProvider abstraction)
│ ├── HotelService.Tests/ # xUnit tests for HotelService business logic
│ ├── CommentsService.Tests/
│ ├── NotificationService.Tests/
│ └── AiAgentService.Tests/
├── frontend/
│ └── src/
│ ├── api/hotelApi.js # All API calls to gateway
│ ├── context/AuthContext.jsx # Supabase Auth session + JWT management
│ ├── pages/ # HomePage, SearchResults, HotelDetail, Booking,
│ │ # MyBookings, AdminPage, LoginPage, SignUpPage
│ └── components/ # Navbar, HotelCard, MapView, AiChatWidget, CommentSection
├── docs/
│ ├── requirements.md # Full functional + non-functional requirements
│ ├── business_process_mapping.md # Business process flows (BP-01 through BP-09)
│ ├── Database-Design-ER-Modeling.md # Full data dictionary and ER documentation
│ └── project-plan.md # Phase-by-phase implementation plan
├── .github/
│ └── workflows/ci.yml # GitHub Actions: build → test → docker build verify
└── README.md
Documentation: docs/ — Full requirements, business process maps, ER modeling, and phase-by-phase plan