Design a Hotel Reservation System that allows users to seamlessly search and book hotels, ensuring a smooth and reliable travel experience while handling scale, performance, and reliability challenges behind the scenes.

Functional Requirements
The following are the core functional requirements of the system:
-
Enable users to search for hotels based on preferences such as location and travel dates.
-
Support the complete booking flow, including room selection, reservation creation, and booking confirmation.
-
Provide secure payment handling with multiple methods, such as credit/debit cards, UPI, wallets, and net banking.
Non-Functional Requirements
The following are the core non-functional requirements of the system:
-
The system must support high volumes of concurrent users, searches, and bookings, with the ability to scale horizontally during peak travel seasons.
-
Search results should be delivered with low latency (e.g., <500 ms for p95), however, it's acceptable for the system to take a few seconds to process a reservation request.
-
Backup and failover mechanisms must exist to recover data and operations quickly in case of outages or regional failures.
Load Estimation
The Hotel Reservation System serves as a platform connecting customers and hotels, handling all stages of the reservation process such as searching for hotels, viewing hotel and room details, and making reservations.
Let’s discuss each use case in detail to gain a clear understanding of the system’s functionality and to evaluate the scale and capacity the platform needs to support.
Use Case 1: Search Hotels
Enables customers to search for available hotels based on their specified location and check-in / check-out dates.
- Assumption: Daily Active Users (DAU) = 5 Million
- Assumption: Average Search Requests per User per Day = 5
- Total Requests per Day = 5M * 5 = 25M
- Average RPS = 25,000,000 / (86,400) ≈ 250 RPS
- Assumption: Peak Load Factor = 5x
- Peak RPS = 250 * 5 ≈ 1,250 RPS
This is a read-heavy, latency-sensitive query workload (high QPS with spiky peaks).
Use Case 2: View Hotel Details
Enables customers to view complete details of a selected hotel, including amenities, images, policies, and room-type availability for the specified check-in and check-out dates.
- Assumption: 50% of users view details after search
- Assumption: Average View Details Requests per User per Day = 2
- Total Requests per Day = 5M * 50% * 2 = 5,000,000
- Average RPS = 5,000,000 / (86,400) ≈ 50 RPS
- Assumption: Peak Load Factor = 5x
- Peak RPS = 50 * 5 ≈ 250 RPS
This is a read-heavy details query (medium QPS).
Use Case 3: Make Reservations
Enables customers to select a hotel and room type, provide guest details, complete payment, and receive a confirmed booking.
- Assumption: 2% of users make a booking per day
- Assumption: Average Booking Requests per User per Day = 1
- Total Requests per Day = 5M * 2% = 100,000
- Average RPS = 100,000 / (24 * 60 * 60) ≈ 1 RPS
- Assumption: Peak Load Factor = 5x
- Peak RPS = 1 * 5 = 5 RPS
This is a classic low-QPS, high-risk use case.
Design Rationale
The use cases exhibit very different operational characteristics in terms of traffic volume, read/write patterns, and consistency requirements.
| Use Case | Avg RPS | Peak RPS | Nature |
|---|---|---|---|
| Search Hotels | 250 | 1250 | Read-heavy, cache-driven |
| View Hotel Details | 50 | 250 | Read-heavy, aggregation |
| Make Reservations | 1 | 5 | Write-heavy, strongly consistent |
These differences make it impractical to serve all use cases through a monolithic architecture.
To avoid tightly coupled workloads with vastly different scalability, latency, and consistency requirements, we will adopt a microservices architecture, where each major use case is implemented as an independent service.
This allows high-traffic, read-heavy services such as search to scale horizontally and leverage caching, while low-traffic but critical workflows like reservations can enforce strong transactional guarantees without being impacted by unrelated load.
Sequence Flow
Let’s understand the step-by-step sequence flow of each of the core use cases to gain deeper insights into how the system works in practice.
Use Case 1: Search Hotels
A straightforward approach would be to let a single SearchService own all data required for hotel search. However, a search query inherently requires combining two fundamentally different categories of data.
-
On one side, it relies on relatively static information such as hotel names, locations, images, amenities, policies, and room-type definitions, that changes infrequently and can be aggressively cached.
-
On the other side, it depends on highly dynamic data like room availability and pricing, which can change frequently due to bookings, demand fluctuations, or pricing rules.
Treating both data types uniformly forces the system to optimize for the most volatile data, which increases latency, reduces cache effectiveness, and wastes compute resources. To avoid this, the system can be decomposed into specialized services.
-
A dedicated
HotelServicecan manage stable hotel profile content and be optimized for read-heavy access and aggressive caching. -
An
InventoryServicecan handle fast-changing room availability with a focus on freshness and concurrency control, while aPricingServicecan manage dynamic pricing logic and frequent updates. -
The
SearchServicecan then act as an orchestrator, coordinating calls to these backend services, retrieving static and dynamic data in parallel, and aggregating the results into a unified search response.
The following interactions describe the step-by-step flow between the user interface and the backend components during this operation:
-
The
Customerenters the search criteria such aslocation,checkInDate,checkOutDateand submits the Search Hotels form. -
The
Frontendsends aGET /hotels/hotel-listingsrequest to theSearchService, including all the selected form parameters:location,checkInDateandcheckOutDate. -
The
SearchServicecoordinates with downstream services to determine hotel availability and pricing.-
Hotel Lookup: Sends a GET request to the
HotelServiceto fetch all hotels listed for the selectedlocation. -
Availability Check: For the list of hotels returned, it queries the
InventoryServiceto identify hotels with at least one available room for the specifiedcheckInDateandcheckOutDate. -
Pricing Calculation: For hotels with availability, it calls the
PricingServiceto retrieve theavgPerNightPricefor the selected date range.
-
-
After aggregating hotel metadata, availability, and pricing information, the
SearchServicecaches the final response (short TTL) and returns a list of available hotels to theFrontend. -
The
Frontendrenders the response as a grid ofHotelcards, displaying key details such as:hotelName,avgPricePerNight,availability,rating, etc.

This design allows each service to scale independently, minimizes performance interference, and ensures fast search responses without compromising the accuracy required for downstream booking flows.
Use Case 2: View Hotel Details
The View Hotel Details use case differs from Search Hotels in both scope and load characteristics. While hotel search is optimized for high-volume traffic and aggregated results across many hotels, viewing hotel details is a low-volume, deep read focused on a single hotel. This interaction typically involves richer data such as detailed descriptions, images, amenities, policies, and room-level information.
Unlike the Search Hotels flow, where the SearchService acts as an orchestrator, the frontend can directly coordinate with the HotelService, InventoryService, and PricingService to render the hotel details page.
The following interactions describe the step-by-step flow between the user interface and the backend components during this operation:
The following interactions describe the step-by-step flow between the user interface and the backend components during this operation:
-
The
Customerselects a hotel from the search results on the user interface. -
The
Frontendissues three asynchronous requests in parallel:-
Sends a
GET /hotels/{hotelId}request to theHotelServiceto retrieve static hotel information such as the hotel name, descriptions, amenities, and policies (typically served from cache, with database fallback). -
Sends a
POST /availabilityrequest to theInventoryServiceto retrieve room-level availability for the selected date range. -
Sends a
POST /pricingrequest to thePricingServiceto calculate and fetch pricing details for each available room type.
-
-
The
Frontendaggregates responses from theHotelService,InventoryService, andPricingService, and renders the Hotel Details Page, displaying hotel profile information along with available room types and their corresponding prices.

This approach avoids unnecessary backend orchestration, reduces end-to-end latency, and simplifies the request flow, while still preserving clean service boundaries and strict data ownership.
Use Case 3: Make Reservations
Using a single ReservationService to handle the entire booking lifecycle (inventory locking, payment coordination, reservation confirmation) may appear simpler, but it introduces significant architectural drawbacks.
The reservation flow involves multiple domains with very different reliability, latency, and consistency characteristics, particularly inventory management and payment processing. Coupling these responsibilities into a single service increases complexity, enlarges the failure blast radius, and makes it harder to reason about correctness and recovery.
To address these issues, the reservation workflow can be designed as a two-part process with split orchestration responsibilities.
Phase 1: Reservation Initiation & Inventory Hold
The ReservationService can orchestrate the booking initiation and inventory locking phase. It can coordinate with the InventoryService to place a temporary hold on the selected inventory, preventing double booking while payment is in progress.
The following interactions describe the step-by-step flow between the user interface and the backend components during this operation:
-
The
Customerselects one or more room types and clicks Book Now on the user interface. -
The
Frontendsends aPOST /reservationsrequest to theReservationService, includingrequestId (idempotency key),hotelId,checkInDate,checkOutDateand selected room types with quantities (e.g.,{ roomTypeA: 2, roomTypeB: 1 }). -
The
ReservationServicefirst checks whether a reservation already exists for the givenrequestId:-
If a matching reservation exists, it returns the existing response.
-
If not, it creates a new reservation record in the INITIATED state.
-
-
The
ReservationServicethen calls theInventoryServicewith a single batch request to place temporary holds for all selected room types and quantities for the given date range. -
The
InventoryServiceverifies availability for each room type and requested quantity:-
If sufficient inventory exists for all requested rooms, it places a temporary inventory hold and returns a hold confirmation.
-
If any room type cannot be fulfilled, the request fails and no holds are created.
-
-
On successful hold creation, the
ReservationServiceupdates the reservation record with the hold details, expiration timestamp, and transitions it to the PENDING_PAYMENT state. -
Finally, the
ReservationServicereturns thereservationIdalong with inventory hold details and the hold expiration timestamp to the frontend, enabling the user to proceed with payment.

Phase 2: Payment Confirmation & Reservation Finalization
Payment processing depends on external systems that are inherently unreliable and slow. Payment gateways may time out, send duplicate callbacks, or complete payments asynchronously. Handling retries, idempotency, webhook verification, and reconciliation are therefore core responsibilities of the PaymentService, not the ReservationService.
By allowing the PaymentService to orchestrate this phase, payment-related failures and retries are isolated from critical reservation state, preventing gateway instability from corrupting booking data. The PaymentService can independently verify the payment with the external gateway, coordinate with the InventoryService to finalize the inventory hold, and then invoke the ReservationService to confirm the reservation.
This sequencing guarantees that reservations are finalized only after successful payment, while keeping payment-specific failure handling fully decoupled from core booking logic.
The following interactions describe the step-by-step flow between the user interface and the backend components during this operation:
-
The
Customerclicks Pay Now on the user interface after reviewing the reservation details. -
The
Frontendsends aPOST /paymentsrequest to thePaymentService, including thereservationId. -
The
PaymentServicevalidates the request, performs an idempotency check, creates a payment order with the external payment gateway and returns the checkout URL along with the payment order details to theFrontend. -
The
Frontendredirects theCustomerto the payment gateway checkout page to complete the payment. -
After the payment is completed, the frontend calls
POST /payments/confirmon thePaymentServicewith thereservationIdand payment verification details. -
The
PaymentServiceverifies the payment status with the payment gateway. Upon successful payment verification, thePaymentServicecalls theInventoryServiceto finalize the inventory hold, converting the temporary hold into a permanent allocation. -
After inventory finalization succeeds, the
PaymentServicecalls theReservationServiceto confirm the reservation, transitioning it from PENDING_PAYMENT to CONFIRMED. -
The
PaymentServicerecords the final payment state and responds to theFrontendwith a successful booking confirmation. -
The
Frontendupdates theCustomer, displaying the booking confirmation and completion status.

This split orchestration model improves fault isolation, scalability, and correctness, while keeping service responsibilities clean and aligned with their respective domains.
API Design
Let’s discuss the API design of the core services in detail, examining the main endpoints each service exposes, the request and response models they use, and the reasoning behind the underlying business logic.
This helps clarify how responsibilities are distributed across services and how they interact to support the reservation flow.
Hotel Service
The Hotel Service is responsible for owning and serving static hotel profile data. This data changes infrequently, is optimized for read-heavy access through aggressive caching with long TTLs, and intentionally excludes dynamic information such as availability and pricing.
Let's discuss the key endpoints exposed by the Hotel Service.
1. GET /hotels
Returns a list of hotels for a given location.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| locationId | String | Yes | City or location identifier (e.g., city code or name) |
Sample Request
GET /hotels?location=BLR
Business Logic
@Cacheable(
value = "hotelsByLocation",
key = "#locationId",
unless = "#result.isEmpty()"
)
public List<HotelView> getHotelsByLocation(String locationId) {
return hotelRepository.findByLocation(locationId)
.stream()
.map(HotelView::from)
.toList();
}
Sample Response
[
{
"hotelId": 101,
"name": "Grand Goa Resort",
"description": "A luxury seaside resort offering modern rooms, infinity
pool, and beach access.",
"locationId": "GOA",
"address": "Candolim Beach Road, Goa, India",
"thumbnailUrl": "https://cdn.example.com/101/thumb.jpg",
"rating": 4.6,
"amenities": ["wifi", "pool", "gym", "spa"]
},
{
"hotelId": 102,
"name": "Westin Resort",
"description": "Premium 5-star property featuring private beach, fine
dining, and wellness spa.",
"locationId": "GOA",
"address": "Arpora Beach Road, Goa, India",
"thumbnailUrl": "https://cdn.example.com/102/thumb.jpg",
"rating": 4.7,
"amenities": ["wifi", "pool", "private beach", "restaurant", "bar"]
}
]
This endpoint is read-optimized and used primarily by the Search Service to fetch static hotel metadata.
2. GET /hotels/{hotelId}
Returns the full hotel profile for a given hotel.
Sample Request
GET /hotels/H123
Business Logic
@Cacheable(
value = "hotelDetails",
key = "#hotelId"
)
public HotelDetailsResponse getHotelDetails(String hotelId) {
Hotel hotel = hotelRepository.findByHotelId(hotelId)
.orElseThrow(() -> new NotFoundException("Hotel not found"));
return HotelDetailsResponse.from(hotel);
}
Sample Response
{
"hotelId": "H123",
"name": "Ocean View Hotel",
"location": "Bangalore",
"description": "A beachfront hotel offering scenic views and premium amenities.",
"rating": 4.5,
"amenities": [
"Free Wi-Fi",
"Swimming Pool",
"Breakfast Included",
"Parking"
],
"imageUrls": [
"https://cdn.example.com/hotels/H123/1.jpg",
"https://cdn.example.com/hotels/H123/2.jpg"
],
"policies": {
"checkInTime": "14:00",
"checkOutTime": "11:00",
"cancellation": "Free cancellation up to 24 hours before check-in"
},
"roomTypes": [
{
"code": "DELUXE",
"name": "Deluxe Room",
"capacity": 2,
"description": "Spacious room with sea view"
},
{
"code": "STANDARD",
"name": "Standard Room",
"capacity": 2,
"description": "Comfortable room with city view"
}
]
}
This endpoint serves static, read-only hotel metadata required to render the hotel details page.
Inventory Service
The Inventory Service manages the complete lifecycle of room inventory, including availability checks, temporary holds during booking, and final confirmation or release of inventory. Its primary responsibility is to enforce strong consistency so that overbooking is impossible even under high concurrency.
Each inventory record represents availability at the granularity of (hotelId, roomType, date).
This allows the system to correctly handle bookings that span multiple dates.
Inventory Record
@Entity
@Getter @Setter
public class Inventory {
@Id
@GeneratedValue
private Long id;
private String hotelId;
private String roomType;
private LocalDate date;
private Integer totalCount;
private Integer reservedCount;
private String holdId;
private Instant holdExpiresAt;
public int getAvailableCount() {
return totalCount - reservedCount;
}
public void hold(String holdId, Instant expiry, int qty) {
this.reservedCount += qty;
this.holdId = holdId;
this.holdExpiresAt = expiry;
}
public void release(int qty) {
this.reservedCount -= qty;
this.holdId = null;
this.holdExpiresAt = null;
}
public void confirm() {
this.holdId = null;
this.holdExpiresAt = null;
}
}
Let's discuss the key endpoints exposed by the Inventory Service.
1. POST /availability/batch
During hotel search, the system checks availability for multiple hotels in batch, allowing the Search Service to efficiently filter eligible hotels without incurring N+1 calls.
Sample Request
POST /availability/batch
Content-Type: application/json
{
"hotelIds": ["H123", "H456", "H789"],
"checkInDate": "2025-03-10",
"checkOutDate": "2025-03-12"
}
Business Logic
@Transactional(readOnly = true)
public BatchAvailabilityResponse checkBatchAvailability(
BatchAvailabilityRequest request
) {
List<Inventory> inventory =
inventoryRepository.findInventoryForHotels(
request.getHotelIds(),
request.getCheckInDate(),
request.getCheckOutDate()
);
Map<String, Boolean> availabilityByHotel = new HashMap<>();
for (String hotelId : request.getHotelIds()) {
boolean hasAvailability = inventory.stream()
.anyMatch(i ->
i.getHotelId().equals(hotelId) &&
i.getAvailableCount() > 0
);
availabilityByHotel.put(hotelId, hasAvailability);
}
return new BatchAvailabilityResponse(availabilityByHotel);
}
Repository Query
@Query("""
SELECT i FROM Inventory i
WHERE i.hotelId IN :hotelIds
AND i.reservationDate >= :checkInDate
AND i.reservationDate < :checkOutDate
""")
List<Inventory> findInventoryForHotels(
List<String> hotelIds,
LocalDate checkInDate,
LocalDate checkOutDate
);
Sample Response
{
"availability": {
"H123": {
"available": true
},
"H456": {
"available": false
},
"H789": {
"available": true
}
}
}
The batch availability API performs a fast, coarse-grained availability check across multiple hotels without room-level details, optimized for the Search Hotels flow.
2. POST /inventory/hold
Places a temporary hold on room inventory to prevent double booking during payment. The hold is time-bound and automatically expires if payment is not completed, making it safe and self-healing.
Sample Request
POST /inventory/hold
Content-Type: application/json
{
"reservationId": "R789",
"hotelId": "H123",
"checkInDate": "2025-03-10",
"checkOutDate": "2025-03-12",
"rooms": {
"DELUXE": 2,
"STANDARD": 1
}
}
Business Logic
@Transactional
public InventoryHoldResponse placeHold(InventoryHoldRequest request) {
List<Inventory> inventory =
inventoryRepository.lockInventory(
request.getHotelId(),
request.getRoomTypes(),
request.getCheckInDate(),
request.getCheckOutDate()
);
// Validate availability using derived availability
for (Inventory item : inventory) {
int requiredQty = request.getQuantity(item.getRoomType());
if (item.getAvailableCount() < requiredQty) {
throw new InventoryUnavailableException();
}
}
String holdId = UUID.randomUUID().toString();
Instant expiry = Instant.now().plus(Duration.ofMinutes(15));
// Apply holds
inventory.forEach(item -> {
int qty = request.getQuantity(item.getRoomType());
item.hold(holdId, expiry, qty);
});
return new InventoryHoldResponse(holdId, expiry);
}
Repository Query
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("""
SELECT i FROM Inventory i
WHERE i.hotelId = :hotelId
AND i.roomType IN :roomTypes
AND i.date BETWEEN :checkIn AND :checkOut
""")
List<Inventory> lockInventory(
String hotelId,
List<String> roomTypes,
LocalDate checkIn,
LocalDate checkOut
);
Sample Response
{
"holdId": "HOLD456",
"expiresAt": "2025-03-10T12:15:00Z"
}
Pessimistic locking is ideal for inventory holds because the operations are write-heavy, highly contended, and require strict correctness guarantees.
3. POST /inventory/confirm
Responsible for converting a temporary inventory hold into a permanent reservation. It is invoked during Phase 2: Payment Processing & Reservation Confirmation, after payment has been successfully verified.
Sample Request
POST /inventory/confirm
Content-Type: application/json
{
"holdId": "HOLD456",
"reservationId": "R789"
}
Business Logic
@Transactional
public InventoryConfirmResponse confirmHold(
InventoryConfirmRequest request
) {
List<Inventory> inventory =
inventoryRepository.findByHoldId(
request.getHoldId()
);
if (inventory.isEmpty()) {
throw new IllegalStateException(
"Hold not found or already released"
);
}
// Idempotency: already confirmed
boolean alreadyConfirmed = inventory.stream()
.allMatch(i -> i.getHoldId() == null);
if (alreadyConfirmed) {
return new InventoryConfirmResponse(
request.getHoldId(),
"CONFIRMED"
);
}
// Validate hold expiry
Instant now = Instant.now();
for (Inventory item : inventory) {
if (item.getHoldExpiresAt().isBefore(now)) {
throw new IllegalStateException(
"Hold expired"
);
}
}
// Finalize inventory
inventory.forEach(Inventory::confirm);
inventoryRepository.saveAll(inventory);
return new InventoryConfirmResponse(
request.getHoldId(),
"CONFIRMED"
);
}
Sample Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"holdId": "HOLD456",
"status": "CONFIRMED"
}
No availability counters are recalculated here. Finalization is a metadata transition, not a quantity change.
4. POST /inventory/release
Responsible for reverting a temporary inventory hold when a booking cannot be completed. This ensures that rooms are returned to the available pool and can be booked by other users.
Sample Request
POST /inventory/release
Content-Type: application/json
{
"holdId": "HOLD456",
"reservationId": "R789",
"reason": "PAYMENT_FAILED"
}
Business Logic
@Transactional
public InventoryReleaseResponse releaseHold(
InventoryReleaseRequest request
) {
List<Inventory> inventory =
inventoryRepository.findByHoldId(
request.getHoldId()
);
// Idempotency: already released
if (inventory.isEmpty()) {
return new InventoryReleaseResponse(
request.getHoldId(),
"RELEASED"
);
}
// Release inventory
inventory.forEach(item -> {
int qty = item.getReservedCountForHold(
request.getHoldId()
);
item.release(qty);
});
inventoryRepository.saveAll(inventory);
return new InventoryReleaseResponse(
request.getHoldId(),
"RELEASED"
);
}
Sample Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"holdId": "HOLD456",
"status": "RELEASED"
}
This endpoint plays a critical role in keeping inventory accurate, self-healing, and leak-free.
Pricing Service
The Pricing Service is responsible for computing room prices for a given hotel, date range, and room type. Key endpoints include:
1. POST /pricing/batch
A bulk endpoint that returns the average per-night price per hotel across multiple hotel IDs.
Sample Response
POST /pricing/batch/
Content-Type: application/json
{
"hotelIds": ["H123", "H456", "H789"],
"checkInDate": "2025-03-10",
"checkOutDate": "2025-03-12"
}
Business Logic
@Transactional(readOnly = true)
public AvgPriceBatchResponse calculateAvgPerNightPrices(
AvgPriceBatchRequest request
) {
Map<String, BigDecimal> priceMap = new HashMap<>();
for (String hotelId : request.getHotelIds()) {
List<BigDecimal> baseRates =
rateRepository.getBaseRatesForHotel(hotelId);
BigDecimal avgBaseRate =
baseRates.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(
BigDecimal.valueOf(baseRates.size()),
RoundingMode.HALF_UP
);
BigDecimal adjustedRate =
ruleEngine.applyRules(
avgBaseRate,
request.getCheckInDate(),
request.getCheckOutDate()
);
priceMap.put(hotelId, adjustedRate);
}
return new AvgPriceBatchResponse("INR", priceMap);
}
Sample Response
{
"currency": "INR",
"prices": {
"H123": {
"avgPerNightPrice": 4200
},
"H456": {
"avgPerNightPrice": 5100
},
"H789": {
"avgPerNightPrice": 3900
}
}
}
This bulk average price API is used only by the Search Service.
2. POST /pricing
Calculates prices for available room types over a date range.
Sample Request
POST /pricing
Content-Type: application/json
{
"hotelId": "H123",
"checkInDate": "2025-03-10",
"checkOutDate": "2025-03-12",
"roomTypes": ["DELUXE", "STANDARD"]
}
Business Logic
@Transactional(readOnly = true)
public PricingResponse calculatePricing(PricingRequest request) {
Map<String, RoomPricing> pricingMap = new HashMap<>();
for (String roomType : request.getRoomTypes()) {
BigDecimal baseRate =
rateRepository.getBaseRate(
request.getHotelId(),
roomType
);
BigDecimal adjustedRate =
ruleEngine.applyRules(
baseRate,
request.getCheckInDate(),
request.getCheckOutDate()
);
long nights =
ChronoUnit.DAYS.between(
request.getCheckInDate(),
request.getCheckOutDate()
);
BigDecimal total = adjustedRate.multiply(
BigDecimal.valueOf(nights)
);
BigDecimal taxes = taxService.calculate(total);
pricingMap.put(
roomType,
new RoomPricing(
adjustedRate,
total,
taxes,
total.add(taxes)
)
);
}
return new PricingResponse(
request.getHotelId(),
"INR",
pricingMap
);
}
Sample Response
{
"hotelId": "H123",
"currency": "INR",
"pricing": {
"DELUXE": {
"perNightPrice": 5000,
"totalPrice": 10000,
"taxes": 1200,
"finalPrice": 11200
},
"STANDARD": {
"perNightPrice": 3500,
"totalPrice": 7000,
"taxes": 840,
"finalPrice": 7840
}
}
}
Search Service
The Search Service is responsible for handling the Search Hotels use case. It acts as a read-optimized, high-throughput service that aggregates data from multiple downstream services to return fast and relevant search results.
Below, we discuss the key endpoints exposed by the Search Service.
1. GET /hotels/hotel-listings
Returns a read-optimized list of available hotels for a given location and date range by aggregating hotel metadata, availability, and pricing.
Request Parameters
| Parameter | Type | Description |
|---|---|---|
| locationId | string | location identifier |
| checkInDate | date | Check-in date |
| checkOutDate | date | Check-out date |
Sample Request
GET /hotels/hotel-listings?locationId=BLR&checkInDate=2025-03-10&checkOutDate=2025-03-12
Business Logic
public List<SearchHotelView> searchHotels(
Long locationId,
LocalDate checkInDate,
LocalDate checkOutDate
) {
// 1. Cache lookup (short TTL)
List<SearchHotelView> cached = cache.get(request);
if (cached != null) {
return cached;
}
// 2. Fetch hotels for location
List<HotelDTO> hotels =
hotelClient.getHotelsByLocation(locationId);
if (hotels.isEmpty()) {
return Collections.emptyList();
}
List<Long> hotelIds =
hotels.stream().map(HotelDTO::getHotelId).toList();
// 3. Batch availability check
AvailabilityResponse availability =
inventoryClient.checkAvailability(
hotelIds,
request.getCheckInDate(),
request.getCheckOutDate()
);
// 4. Filter available hotels
List<String> availableHotelIds =
availability.getAvailableHotelIds();
if (availableHotelIds.isEmpty()) {
return Collections.emptyList();
}
// 5. Batch pricing call
Map<String, BigDecimal> pricing =
pricingClient.getAvgPrices(
availableHotelIds,
request.getCheckInDate(),
request.getCheckOutDate()
);
// 6. Aggregate response
List<SearchHotelView> results = hotels.stream()
.filter(h -> availableHotelIds.contains(h.getHotelId()))
.map(h -> SearchHotelView.from(h, pricing.get(h.getHotelId())))
.toList();
// 7. Cache result (short TTL)
cache.put(request, response);
return results;
}
Sample Response
[
{
"hotelId": 101,
"name": "Grand Goa Resort",
"address": "Candolim Beach Road, Goa, India",
"thumbnailUrl": "https://cdn.example.com/101/thumb.jpg",
"rating": 4,
"avgPricePerNight": 4500,
},
{
"hotelId": 102,
"name": "City Budget Inn",
"address": "MG Road, Bengaluru, India",
"thumbnailUrl": "https://cdn.example.com/102/thumb.jpg",
"rating": 3,
"avgPricePerNight": 2200,
}
]
Reservation Service
The Reservation Service is responsible for managing the booking lifecycle, including reservation creation, inventory locking coordination, and reservation state transitions.
It owns the reservation state machine and ensures that bookings are created safely before payment is initiated. Below are the reservation states managed by the Reservation Service:
INITIATED: Reservation record created (idempotency anchor)PENDING_PAYMENT: Inventory successfully held, awaiting paymentCONFIRMED: Booking finalized after successful paymentEXPIRED/FAILED: Reservation invalidated due to timeout or failure
Now, let's discuss the key endpoints exposed by the Reservation Service.
1. POST /reservations
Creates a new reservation request and initiates Phase 1: Reservation Initiation & Inventory Hold.
Request Parameters
| Field | Type | Description |
|---|---|---|
| requestId | string | Client-generated idempotency key |
| hotelId | string | Selected hotel identifier |
| checkInDate | date | Check-in date |
| checkOutDate | date | Check-out date |
| rooms | map | Room types with requested quantities |
Sample Request
{
"requestId": "req-12345",
"hotelId": "H123",
"checkInDate": "2025-03-10",
"checkOutDate": "2025-03-12",
"rooms": {
"DELUXE": 2,
"STANDARD": 1
}
}
Business Logic
@Transactional
public Reservation createReservation(
String requestId,
Long hotelId,
LocalDate checkInDate,
LocalDate checkOutDate,
Map<String, Integer> rooms,
) {
// 1. Idempotency check
Optional<Reservation> existing =
reservationRepository.findByRequestId(requestId);
if (existing.isPresent()) {
return ReservationResponse.from(existing.get());
}
// 2. Create reservation record (INITIATED)
Reservation reservation = Reservation.builder()
.requestId(requestId)
.hotelId(getHotelId)
.checkInDate(checkInDate)
.checkOutDate(checkOutDate)
.rooms(rooms)
.status(ReservationStatus.INITIATED)
.build();
reservationRepository.save(reservation);
// 3. Call InventoryService (batch hold)
InventoryHoldResponse holdResponse =
inventoryClient.placeHold(
InventoryHoldRequest.from(reservation)
);
// 4. Update reservation on successful hold
reservation.setStatus(ReservationStatus.PENDING_PAYMENT);
reservation.setHoldId(holdResponse.getHoldId());
reservation.setHoldExpiresAt(holdResponse.getExpiresAt());
reservationRepository.save(reservation);
return reservation;
}
Sample Response
{
"reservationId": "R789",
"status": "PENDING_PAYMENT",
"inventoryHold": {
"holdId": "HOLD456",
"expiresAt": "2025-03-10T12:15:00Z"
}
}
@Transactional guarantees that the idempotency check, reservation record creation, and state transition from INITIATED to PENDING_PAYMENT are committed together, preventing partially persisted or inconsistent booking state.
If any step fails, the transaction is rolled back automatically, preserving data integrity.
2. POST /reservations/{reservationId}/confirm
Finalizes a reservation after successful payment and inventory finalization.
Sample Request
POST /reservations/R789/confirm
Content-Type: application/json
{
"paymentId": "pay_987654",
"orderId": "order_123456",
"paymentProvider": "RAZORPAY",
"paidAt": "2025-03-10T10:15:30Z"
}
Business Logic
@Transactional
public ReservationConfirmationResponse confirmReservation(
String reservationId,
PaymentConfirmationRequest request
) {
Reservation reservation = reservationRepository
.findById(reservationId)
.orElseThrow(() ->
new NotFoundException("Reservation not found")
);
// Idempotency: already confirmed
if (reservation.getStatus() == ReservationStatus.CONFIRMED) {
return ReservationConfirmationResponse.from(reservation);
}
// Validate state
if (reservation.getStatus() != ReservationStatus.PENDING_PAYMENT) {
throw new IllegalStateException(
"Invalid state transition for reservation: " + reservation.getStatus()
);
}
// Persist payment details
reservation.setPaymentId(request.getPaymentId());
reservation.setPaymentOrderId(request.getOrderId());
reservation.setPaymentProvider(request.getPaymentProvider());
reservation.setPaidAt(request.getPaidAt());
// Transition state
reservation.setStatus(ReservationStatus.CONFIRMED);
reservation.setConfirmedAt(Instant.now());
reservationRepository.save(reservation);
return ReservationConfirmationResponse.from(reservation);
}
If duplicate or retried confirmation requests arrive (which is common with payment callbacks), @Transactional ensures that state checks and updates are executed consistently, so the reservation cannot be confirmed twice or left in an intermediate state.
Sample Response
{
"reservationId": "R789",
"status": "CONFIRMED",
"confirmedAt": "2025-03-10T10:15:35Z"
}
This endpoint is invoked only by the PaymentService during Phase 2: Payment Processing & Reservation Confirmation.
Payment Service
The Payment Service is responsible for payment orchestration. It manages interactions with external payment gateways, handles retries and callbacks, and coordinates downstream services to safely finalize a booking after payment.
Let's discuss the key endpoints exposed by the Payment Service.
1. POST /payments/order
Creates a payment gateway order and returns a checkout URL.
Sample Request
POST /payments/order
Content-Type: application/json
{
"reservationId": "R789",
"amount": 11200,
"currency": "INR",
"paymentProvider": "RAZORPAY",
"returnUrl": "https://app.example.com/payment/return"
}
Business Logic
@Transactional
public PaymentOrderResponse createOrder(
PaymentOrderRequest request
) {
// 1. Validate reservation state
ReservationSnapshot reservation =
reservationClient.getReservation(
request.getReservationId()
);
if (!reservation.isPendingPayment()) {
throw new IllegalStateException(
"Reservation not eligible for payment"
);
}
// 2. Idempotency: existing payment order
Optional<Payment> existing =
paymentRepository.findByReservationId(
request.getReservationId()
);
if (existing.isPresent()) {
return PaymentOrderResponse.from(existing.get());
}
// 3. Create payment order with gateway
GatewayOrder gatewayOrder =
gatewayClient.createOrder(
request.getAmount(),
request.getCurrency(),
request.getReturnUrl()
);
// 4. Persist payment record
Payment payment = new Payment();
payment.setReservationId(request.getReservationId());
payment.setPaymentOrderId(gatewayOrder.getOrderId());
payment.setStatus(PaymentStatus.CREATED);
payment.setAmount(request.getAmount());
payment.setCurrency(request.getCurrency());
payment.setProvider(request.getPaymentProvider());
payment.setCreatedAt(Instant.now());
paymentRepository.save(payment);
// 5. Return checkout details
return PaymentOrderResponse.from(payment, gatewayOrder);
}
Sample Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"paymentOrderId": "order_123456",
"checkoutUrl": "https://checkout.razorpay.com/v1/checkout.js?order_id=order_123456",
"expiresAt": "2025-03-10T12:15:00Z"
}
This endpoint is invoked when the user clicks “Pay Now” and marks the start of Phase 2: Payment Processing.
2. POST /payments/confirm
Verifies payment and finalizes the booking.
Sample Request
POST /payments/confirm
Content-Type: application/json
{
"reservationId": "R789",
"paymentOrderId": "order_123456",
"paymentId": "pay_987654",
"paymentProvider": "RAZORPAY",
"signature": "gateway_signature_value"
}
Business Logic
@Transactional
public PaymentConfirmationResponse confirmPayment(
PaymentConfirmRequest request
) {
Payment payment = paymentRepository
.findByPaymentOrderId(request.getPaymentOrderId())
.orElseThrow(() ->
new IllegalStateException("Payment order not found")
);
// Idempotency: already processed
if (payment.getStatus() == PaymentStatus.SUCCESS) {
return PaymentConfirmationResponse.success(
payment.getReservationId()
);
}
// 1. Verify payment with gateway
gatewayClient.verifyPayment(
request.getPaymentOrderId(),
request.getPaymentId(),
request.getSignature()
);
// 2. Finalize inventory
inventoryClient.confirmHold(payment.getHoldId());
// 3. Confirm reservation
reservationClient.confirmReservation(
payment.getReservationId(),
request.getPaymentId(),
request.getPaymentOrderId(),
request.getPaymentProvider()
);
// 4. Update payment state
payment.setStatus(PaymentStatus.SUCCESS);
payment.setPaymentId(request.getPaymentId());
payment.setCompletedAt(Instant.now());
paymentRepository.save(payment);
return PaymentConfirmationResponse.success(
payment.getReservationId()
);
}
Sample Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"reservationId": "R789",
"paymentStatus": "SUCCESS",
"bookingStatus": "CONFIRMED"
}
This endpoint is invoked after the user completes payment (via redirect or webhook trigger). The Payment Service acts as the orchestrator, ensuring that booking is finalized only after payment is confirmed.
Data Model Design
Different parts of the system evolve at very different rates and under different constraints. For example, hotel profile information such as names, amenities, and images changes infrequently and is primarily read-heavy, while inventory and reservation data changes constantly and must be updated with strong consistency under high concurrency.
By designing schemas around these change characteristics, each service can use storage structures, indexing strategies, and transactional guarantees that best fit its data. This approach naturally leads to different schemas and storage patterns across services, avoids forcing one-size-fits-all models, and prevents slow-moving, read-heavy data from interfering with fast-changing, correctness-critical workflows like booking and payment.
Let’s examine the data models for each core service, focusing on the queries they must support, how their data volumes are expected to grow over a 10-year horizon, and the partitioning, indexing, and storage strategies required to sustain scalability, reliability, and performance at scale.
Hotel Service
The Hotel Service queries are primarily read-heavy and designed to return results quickly with low latency. They do not require strict correctness guarantees because they deal with static or slow-changing data, and updates are rare and infrequent, meaning concurrent writes are uncommon. This makes them well suited for caching and read-optimized database access.
Schema Design

Since this data changes infrequently and does not participate in transactional booking workflows, its schema is intentionally kept simple and normalized, modeling only static metadata.
Database Choice
A relational database works well for the Hotel Service because the data is clearly structured and naturally fits into tables with relationships.
In addition, schema changes are easy to manage for slow-changing data, and constraints like unique hotelId or (hotelId, roomType) can be enforced directly at the database level, keeping the data clean and reliable with minimal application logic.
Indexing Strategy
The Hotel Service indexing strategy focuses on keeping read queries fast and predictable.
A primary key and unique index on hotelId enable quick access to individual hotel profiles, while a composite index on (hotelId, roomTypeCode) allows efficient retrieval of room-type definitions for a specific hotel.
For search queries that filter hotels by location, relying on a full table scan would be inefficient at scale and lead to high I/O and inconsistent latency. Introducing a B-tree index on locationId allows the database to locate matching rows directly through index lookups, avoiding full scans and significantly improving performance for low-latency search endpoints.
Data Growth (10-Year Horizon)
Hotel metadata grows slowly and in a highly predictable manner, driven primarily by the onboarding of new hotels rather than frequent updates to existing records.
- Assumption: Total Hotels over 10-Year span = 1 Million
- Average Hotel Record Size = 1 KB (with indexes)
- Total Storage for Hotels = 1,000,000 × 1 KB = 1 GB
Each hotel is associated with a bounded and fixed set of room types, amenities, and images.
There are no unbounded collections or time-series–style inserts in this model. As a result, even over a 10-year horizon and at large scale, the overall data footprint remains modest and easy to manage compared to dynamic datasets such as inventory or reservations.
Partitioning Strategy
Partitioning is not required initially due to low write volume and predictable growth. If needed at very large scale, partitioning or sharding can be introduced by location (regional sharding).
Inventory Service
The Inventory Service queries are write-heavy and highly contended, with multiple users often trying to update the same inventory records at the same time.
Schema Design

The schema is designed to support strong consistency, fine-grained updates, and high write contention, while still enabling efficient availability queries for search and booking flows.
Database Choice
A relational database is well suited for the Inventory Service because it provides strong transactional guarantees and supports row-level locking, which are essential for handling concurrent inventory holds safely.
Pessimistic locking mechanisms like SELECT … FOR UPDATE allow the system to serialize competing booking requests and prevent race conditions that could lead to overbooking. Since inventory correctness is more important than raw throughput, a strongly consistent relational database is a better choice than NoSQL or eventually consistent stores, which could allow conflicting updates and compromise booking accuracy.
Indexing Strategy
Search and booking operations routinely filter inventory by hotelId, roomType, and date. Without proper indexing, these queries would degrade into sequential scans over millions of rows, especially when validating availability across multiple consecutive dates. This would lead to high I/O, lock contention, and unpredictable latency.
By introducing a composite B-tree index on (hotelId, roomType, date), the database can efficiently locate the exact inventory rows required and perform fast range scans over booking windows, ensuring consistent performance for both search and booking workflows.
An optional index on holdId supports quick lookup during confirm and release operations, which are triggered frequently during payment success, failure, or timeout flows.
Data Growth (10-Year Horizon)
As the number of hotels grows, inventory data expands multiplicatively across room types and calendar days, making it the fastest-growing dataset in the system.
- Assumptions:
- Total Hotels = 1,000,000
- Avg Room Types per Hotel = 3 (Standard, Deluxe, Suite)
- Days per Year = 365
- Rows per Year
= 1,000,000 × 3 × 365
≈ 1,095,000,000 ≈ 1.1 - 1.2 Billion rows
- Avg Row Size = ~50 bytes
- Total Storage (10-Year Horizon)
= 10 × 1,200,000,000 × 50 bytes
≈ 600,000,000,000 bytes ≈ 0.6 TB
Because inventory is modeled at the (hotel × roomType × date) level, even modest growth in hotels results in massive data expansion over time. This makes inventory the largest and most rapidly growing dataset in the platform.
Partitioning Strategy
Partitioning the Inventory table by date on a monthly basis is a practical and scalable approach, given the data’s time-series nature and rapid growth. By organizing records into monthly partitions (for example, inventory_2025_01, inventory_2025_02, and so on), the system keeps active inventory data for current and upcoming dates compact and efficient to query, while naturally isolating older data.
Most availability checks, inventory holds, and booking updates operate on a small future window, typically the next 30–90 days. With monthly partitioning, the database only scans the partitions whose date ranges overlap the query, instead of the entire table. This significantly reduces I/O, lowers lock contention, and improves overall query performance under concurrent load.
From an operational perspective, recent partitions, such as the next 6–8 months, can be kept on fast, writable storage and tuned for low-latency reads and writes, since this is where the majority of search and booking traffic is concentrated. Older partitions, which are rarely accessed for live bookings, can be archived or moved to cheaper storage, keeping the primary database lean and performant while still preserving historical data when needed.
Pricing Service
The Pricing Service queries are mostly read-heavy and focused on computation, with very little write activity or contention.
Schema Design

The schema is designed to support fast, read-heavy access and deterministic price calculations, while remaining small, stable, and easy to evolve as pricing rules change.
Database Choice
A relational database works well for the Pricing Service because the data is structured and easy to model with tables and relationships. While strong consistency is available, it is not heavily stressed since pricing data changes infrequently.
Schema changes are simple to manage over time, and index-based lookups provide fast and predictable reads. Because writes are rare, the database easily handles the workload without performance issues.
Indexing Strategy
A composite index on (hotelId, roomType) enables quick base-rate lookups, while optional date-range indexes support efficient rule evaluation. Frequently accessed pricing data can also be cached in memory to further reduce database load.
Data Growth (10-Year Horizon)
Pricing data grows slowly and in a very controlled way. Each hotel typically has only one base rate per room type, the number of pricing rules is limited, and there are no per-booking or time-series records generated. Because of this, even as the platform scales over many years, the overall size of pricing data remains small and manageable, especially when compared to rapidly growing datasets like inventory or reservations.
Partitioning Strategy
Partitioning is not needed at the beginning because pricing data is small and access patterns are stable. If the system grows significantly in the future, room_rates can be sharded by hotelId.
However, most scalability for the Pricing Service comes from caching and stateless price computation, rather than complex database partitioning.
Reservation Service
The Reservation Service queries tend to be write-heavy during booking spikes, when many users are creating or updating reservations at the same time. After a booking is completed, the access pattern shifts and becomes read-heavy, as users and support systems frequently fetch reservation details for viewing, confirmation, or troubleshooting.
Schema Design

The schema is designed to support safe state transitions, idempotent creation, and strong consistency, while handling bursty traffic patterns during booking flows.
Database Choice
A relational database is well suited for the Reservation Service because it provides strong transactional guarantees needed for safe state transitions, enforces uniqueness and correctness through constraints and indexes, and prevents partial updates during failures using ACID transactions.
Since reservation data is business-critical and must remain consistent and auditable at all times, eventually consistent data stores are avoided to ensure the reservation state is never ambiguous.
Indexing Strategy
A unique index on requestId ensures idempotent reservation creation, while the primary key on reservationId enables fast lookups of individual bookings. An index on status supports background jobs such as expiry and cleanup, and optional indexes on hotelId and createdAt help with reporting and support queries.
Together, these indexes provide efficient reads while keeping write overhead at a manageable level.
Data Growth (10-Year Horizon)
Because reservation records are transactional, they are accessed most frequently within the first 12 months after creation (for cancellations, modifications, customer support queries, chargebacks, and dispute resolution). Beyond this window, access frequency drops sharply, making older records far less relevant for day-to-day operations.
- Assumption: Daily Bookings = 100,000
- Assumption: Retention Window = 1 year (365 days)
- Total Bookings per Year
= 100,000 × 365
= 36.5 Million
- Avg Booking Record Size = ~1 KB (including indexes and overhead)
- Total Storage per Year
= 36,500,000 × 1 KB
≈ 36.5 GB
Reservation data grows steadily and predictably as booking volume increases over time. While it is much larger than static datasets such as hotel metadata or pricing rules, it remains significantly smaller than inventory data, which multiplies across dates and room types.
Partitioning Strategy
Reservations can be partitioned by creation time, typically using monthly or yearly partitions, to support long-term scalability as booking volume grows.
Recent partitions contain active and frequently accessed reservations, which keeps day-to-day queries fast and predictable. As reservations age and access frequency drops, older partitions can be archived or moved to cold storage without impacting live traffic.
Payment Service
The Payment Service queries are write-heavy when payments are created and confirmed, and read-heavy later for verification, retries, and customer or support lookups.
While these operations are generally not extremely latency-sensitive, they are reliability-critical, because payment state must always be correct and recoverable, even in the presence of retries, failures, or duplicate callbacks from external payment gateways.
Schema Design

The schema models the full lifecycle of a payment, from creation to completion or failure. Each payment record represents a single payment intent tied to a reservation.
Database Choice
A relational database is well suited for the Payment Service because it provides strong consistency, supports unique constraints to prevent duplicate processing.
Eventual consistency is avoided since payment state must always be accurate, deterministic, and auditable.
Indexing Strategy
A unique index on paymentOrderId ensures idempotency and prevents duplicate processing, while an index on reservationId allows quick correlation with booking data across services.
An index on status supports operational tasks such as retries and monitoring, and an optional index on createdAt enables reporting and audit queries.
Together, these indexes provide fast and reliable lookups while keeping write overhead low and manageable.
Data Growth (10-Year Horizon)
Payment data grows steadily over time, increasing in proportion to the number of bookings. Each reservation typically generates one or more payment records, which must be retained for long periods to meet compliance and auditing requirements.
While the dataset becomes large over time, it remains predictable and manageable, and is much smaller than inventory data, though similar in size to reservation data.
Partitioning Strategy
Payment data is partitioned by creation time (monthly or yearly) so that recent, active payments stay in fast hot partitions, while older records can be archived or moved to cold storage.
This time-based partitioning makes it easier to manage long-term data retention, keeps day-to-day queries fast, and helps meet auditing and regulatory requirements as the system grows.
Bandwidth Estimation
Bandwidth estimation ties your logical design to capacity planning and infra decisions.
By analyzing each use case independently, we can understand the ingress and egress traffic patterns for the services involved and estimate the overall network load the system must handle. This helps us reason about instance sizing, horizontal scaling, and where supporting infrastructure such as CDNs, caches, or asynchronous queues can significantly reduce cost and improve performance.
Search Hotels
In the Search Hotels use case, the dominant bandwidth cost comes from images. If images are served directly as part of the search response, the payload size becomes very large.
- Assumption: 10 images returned per request
- Single Hotel Metadata Size ≈ 5 KB
- Single Image Size ≈ 500 KB
- Payload Size per Request
= 5 KB + (10 × 500 KB)
≈ 5 MB
- Average RPS ≈ 250
- Average Throughput
= 5 MB × 250 RPS
= 1,250 MB/s
≈ 10,000 Mbps (~10 Gbps)
- Peak RPS ≈ 1,250
- Peak Throughput
= 5 MB × 1,250 RPS
= 6,250 MB/s
≈ 50,000 Mbps (~50 Gbps)
Serving images directly from backend APIs would require tens of Gbps of bandwidth at peak, which is impractical and expensive. This calculation strongly justifies serving images via a CDN and returning only image URLs in API responses.
View Hotel Details
The View Hotel Details use case returns richer metadata but for far fewer hotels per request. Pagination limits the payload size and keeps bandwidth requirements manageable.
- Assumption: 20 hotel records per request (pagination)
- Single Hotel Record Size ≈ 5 KB
- Payload Size per Request
= 20 × 5 KB
= 100 KB
- Average RPS ≈ 50
- Average Throughput
= 100 KB × 50 RPS
= 5,000 KB/s
≈ 5 MB/s
≈ 40 Mbps
- Peak RPS ≈ 250
- Peak Throughput
= 100 KB × 250 RPS
= 25,000 KB/s
≈ 25 MB/s
≈ 200 Mbps (~0.2 Gbps)
Compared to search-with-images, this use case is moderate in bandwidth usage and very manageable with standard horizontal scaling and caching.
Make Reservations
The Make Reservation use case is bandwidth-light but correctness-critical. Payloads are small and consist mostly of structured data.
- Assumption: Reservation request includes hotelId, dates,
room types, quantities, and idempotency key
- Request Payload Size ≈ 2 KB
- Response Payload Size ≈ 2 KB
- Average RPS ≈ 1
- Average Throughput
≈ 4 KB × 1 RPS
= 4 KB/s (negligible)
- Peak RPS ≈ 5
- Peak Throughput
≈ 4 KB × 4 RPS
= 20 KB/s
Reservation traffic is negligible from a bandwidth perspective. The service is not network-bound but database and correctness-bound.
Architecture Design
Let’s do a service-by-service discussion of the final architecture, grounded in AWS infrastructure and aligned with the workload, scaling, and correctness requirements identified earlier.
Search Service
Handles the highest traffic volume and sits at the front of the system. It is a read-optimized aggregator that coordinates with Hotel, Inventory, and Pricing services.
- Assumptions:
- One ECS task can safely handle 80–100 RPS (aggregation + cache lookups)
- Peak Search Load ≈ 1,250 RPS
- Target utilization ≈ 70–75% for headroom
- Required instances at peak
≈ 1,250 RPS / 90 RPS per instance
≈ 14 instances
With aggressive caching and image delivery offloaded to CloudFront, the service is primarily compute-bound. To handle a peak load of ~1250 RPS, the system typically requires 12 - 15 ECS instances, spread across AZs for fault tolerance.
Hotel Service
The Hotel Service peak load is primarily driven by the Search Hotels use case, not by individual hotel detail views. During a search, the Search Service needs to fetch hotel metadata for many hotels in a single request, which creates a fan-out effect toward the Hotel Service.
- Assumptions:
- Avg hotels returned per search (location-based) = 20
- Hotel metadata fetched in batches (not one call per hotel)
- Assumption: Batch size per Hotel Service call = 10 hotels
- Safe capacity per instance ≈ 150–200 RPS
- Peak Search RPS = 1,250
- Hotel Service calls per search
= 20 / 10
= 2 calls
- Hotel Service Peak RPS
= Search Peak RPS × Calls per search
= 1,250 × 2
= 2,500 RPS
- Required instances
= 2,500 RPS / 175 RPS per instance
≈ 14 instances
With a high cache hit rate via ElastiCache, the Hotel Service can handle traffic efficiently with 12 - 15 ECS instances, backed by an RDS database with read replicas.
Inventory Service
In the Search Hotels flow, the Search Service must determine which hotels have availability for a given date range. Since availability is dynamic and correctness-critical, this data cannot be embedded or denormalized into the Search or Hotel services. As a result, the Search Service must call the Inventory Service, creating fan-out.
However, unlike Hotel metadata, Inventory fan-out must be tightly controlled, because Inventory is write-heavy, highly contended, and database-bound.
- Assumptions:
- Inventory calls per search = 1 (batched)
- Safe instance capacity ≈ 150 - 200 RPS
- Inventory Peak RPS (search-driven)
= Search Peak RPS × Inventory calls per search
= 1,250 × 1
= 1,250 RPS
- Required instances
= 1,250 RPS / 175 RPS per instance
≈ 7 - 8 instances
Scaling is conservative and driven by database throughput rather than RPS. Typically, 7 - 8 ECS instances are sufficient, with careful tuning of connection pools and locking behavior.
Pricing Service
A stateless, compute-focused service that calculates prices on demand. Because pricing logic is isolated in the PricingService, the SearchService must call it during search, which introduces fan-out.
However, the pricing fan-out is much easier to control than the inventory fan-out because pricing data is read-heavy, stateless, and cacheable.
- Assumptions:
- Avg hotels per search result = 20
- Pricing calls per search = 1 (bulk)
- Safe instance capacity ≈ 300 - 400 RPS
- Peak Search RPS = 1,250
- Pricing Peak RPS (search-driven)
= Search Peak RPS × Pricing calls per search
= 1,250 × 1
= 1,250 RPS
Required instances
= 1,250 RPS / 350 RPS per instance
≈ 4 instances
For the target load, 3 - 4 ECS instances are usually enough to handle both search-time bulk pricing and booking-time validation.
Reservation Service
Owns the booking lifecycle and reservation state machine. Traffic is low but correctness-critical and transactional.
- Assumptions:
- Safe instance capacity ≈ 10 - 15 RPS
- Peak Reservation RPS = 5
Required instances
= 5 RPS / 12 RPS per instance
≈ 1 instance
The service is database-bound rather than network-bound, and typically requires 2 - 3 ECS instances for redundancy and steady performance.
Payment Service
Integrates with external payment gateways and handles retries, callbacks, and asynchronous confirmations. Traffic volume is low, but reliability is paramount.
Assumptions:
- Payment success rate ≈ 95%
- Safe instance capacity ≈ 10 - 15 RPS
- Reservation RPS ≈ 5
- Pay Now RPS = 5 × 0.95 ≈ 5 RPS
- Required instances
= 6 RPS / 12 RPS per instance
≈ 1 instance
A small fleet of 2 - 3 ECS instances is sufficient, with scaling driven by failure handling rather than throughput.
Final AWS Architecture
