Loading learning content...
Design patterns aren't academic exercises—they're proven solutions to recurring problems. In the Hotel Booking System, three patterns are particularly valuable:
This page shows how each pattern solves real problems in our domain, with complete implementations you can adapt.
By the end of this page, you will understand when and how to apply Factory, State, and Template Method patterns in a hotel booking context. More importantly, you'll see why these patterns were chosen—the problems they solve and the extensibility they enable.
The Problem:
Creating reservations involves complex logic:
Scattering this logic across controllers or services leads to inconsistency and duplication. The Factory Pattern centralizes creation logic, ensuring all reservations are created correctly regardless of the entry point.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
/** * ReservationFactory - centralizes complex reservation creation */class ReservationFactory { private readonly hotelRepository: HotelRepository; private readonly guestRepository: GuestRepository; private readonly pricingEngine: PricingEngine; private readonly inventoryService: InventoryService; private readonly confirmationGenerator: ConfirmationNumberGenerator; constructor(deps: ReservationFactoryDependencies) { this.hotelRepository = deps.hotelRepository; this.guestRepository = deps.guestRepository; this.pricingEngine = deps.pricingEngine; this.inventoryService = deps.inventoryService; this.confirmationGenerator = deps.confirmationGenerator; } /** * Create a new reservation from booking input * This is the single entry point for all reservation creation */ async create(input: CreateReservationInput): Promise<Reservation> { // Step 1: Validate and fetch related entities const hotel = await this.validateAndGetHotel(input.hotelId); const roomType = await this.validateAndGetRoomType(hotel, input.roomTypeId); const guest = await this.getOrCreateGuest(input.guest); // Step 2: Validate dates this.validateDates(input.checkInDate, input.checkOutDate, hotel); // Step 3: Check and reserve availability await this.reserveInventory(hotel, roomType, input); // Step 4: Calculate complete pricing const pricing = await this.calculatePricing(hotel, roomType, input); // Step 5: Generate confirmation number const confirmationNumber = this.confirmationGenerator.generate(hotel); // Step 6: Build the reservation entity const reservation = new Reservation({ id: ReservationId.generate(), confirmationNumber, hotelId: hotel.id, roomTypeId: roomType.id, primaryGuestId: guest.id, checkInDate: input.checkInDate, checkOutDate: input.checkOutDate, guestCount: new GuestCount( input.adults, input.children, input.childAges ), roomCount: input.roomCount, roomCharges: pricing.nightlyBreakdown, totalRoomCharge: pricing.roomSubtotal, taxAmount: pricing.taxes.totalTax, fees: pricing.fees.items, promotionsApplied: pricing.promotionsApplied, totalAmount: pricing.grandTotal, cancellationPolicy: pricing.cancellationPolicy, ratePlanCode: input.ratePlanCode, channel: input.channel, source: input.source, specialRequests: input.specialRequests || [], guestNotes: input.guestNotes, }); // Step 7: Initial state is PENDING (awaiting payment) // State already set in constructor return reservation; } /** * Create reservation from OTA data (different input format) */ async createFromOTA( channel: BookingChannel, otaData: OTAReservationData ): Promise<Reservation> { // Translate OTA-specific format to our standard format const standardInput = this.translateOTAData(channel, otaData); // OTA bookings typically come pre-confirmed const reservation = await this.create(standardInput); // Set external reference for tracking reservation.setExternalReference( channel, otaData.otaConfirmationNumber ); // OTA bookings are already confirmed (payment handled by OTA) if (otaData.paymentGuaranteed) { reservation.confirm(); } return reservation; } /** * Create walk-in reservation (different flow) */ async createWalkIn(input: WalkInInput): Promise<Reservation> { // Walk-ins use today's date, immediate check-in const standardInput: CreateReservationInput = { ...input, checkInDate: LocalDate.now(), channel: BookingChannel.DIRECT, source: 'Walk-in', }; const reservation = await this.create(standardInput); // Walk-ins are immediately confirmed and checked in reservation.confirm(); return reservation; } // ============ Helper Methods ============ private async validateAndGetHotel(hotelId: HotelId): Promise<Hotel> { const hotel = await this.hotelRepository.findById(hotelId); if (!hotel) { throw new HotelNotFoundError(hotelId); } if (!hotel.isAcceptingBookings) { throw new HotelNotAcceptingBookingsError(hotelId); } return hotel; } private async validateAndGetRoomType( hotel: Hotel, roomTypeId: RoomTypeId ): Promise<RoomType> { const roomType = hotel.getRoomType(roomTypeId); if (!roomType) { throw new RoomTypeNotFoundError(roomTypeId, hotel.id); } if (!roomType.isActive) { throw new RoomTypeNotAvailableError(roomTypeId); } return roomType; } private validateDates( checkIn: LocalDate, checkOut: LocalDate, hotel: Hotel ): void { if (!checkOut.isAfter(checkIn)) { throw new InvalidDateRangeError( 'Check-out must be after check-in' ); } const today = LocalDate.now(hotel.timezone); if (checkIn.isBefore(today)) { throw new InvalidDateRangeError( 'Cannot book dates in the past' ); } // Maximum advance booking (e.g., 365 days) const maxAdvance = today.plusDays(365); if (checkIn.isAfter(maxAdvance)) { throw new InvalidDateRangeError( 'Cannot book more than 365 days in advance' ); } } private async getOrCreateGuest( guestInput: GuestInput ): Promise<Guest> { // Try to find existing guest by email let guest = await this.guestRepository.findByEmail(guestInput.email); if (guest) { // Update contact info if changed guest.updateContactInfo(guestInput.phone); return guest; } // Create new guest return new Guest({ id: GuestId.generate(), firstName: guestInput.firstName, lastName: guestInput.lastName, email: guestInput.email, phone: guestInput.phone, }); } private async reserveInventory( hotel: Hotel, roomType: RoomType, input: CreateReservationInput ): Promise<void> { const available = await this.inventoryService.checkAvailability( hotel.id, roomType.id, input.checkInDate, input.checkOutDate ); if (available.availableRooms < input.roomCount) { throw new InsufficientInventoryError( `Requested ${input.roomCount} rooms, ` + `only ${available.availableRooms} available` ); } // Reserve inventory (will be released on failure/cancellation) await this.inventoryService.reserveInventory( hotel.id, roomType.id, input.checkInDate, input.checkOutDate, input.roomCount, ReservationId.generate() // Temporary ID for logging ); } private async calculatePricing( hotel: Hotel, roomType: RoomType, input: CreateReservationInput ): Promise<PricingResult> { const ratePlan = await this.getRatePlan(input.ratePlanCode, hotel.id); return this.pricingEngine.calculatePricing({ hotel, roomType, ratePlan, stayDates: new DateRange(input.checkInDate, input.checkOutDate), nights: input.checkInDate.until(input.checkOutDate, ChronoUnit.DAYS), adults: input.adults, children: input.children, promoCode: input.promotionCode, currency: input.currency || hotel.defaultCurrency, parkingRequested: input.parkingRequested ?? false, parkingType: input.parkingType ?? 'SELF', hasPets: input.hasPets ?? false, context: { bookingDate: LocalDate.now(), stayDates: new DateRange(input.checkInDate, input.checkOutDate), channel: input.channel, guest: await this.guestRepository.findByEmail(input.guest.email), }, }); }} /** * Abstract Factory for creating different room type variants */abstract class RoomTypeFactory { abstract createRoomType(params: RoomTypeParams): RoomType; // Shared validation logic protected validate(params: RoomTypeParams): void { if (!params.code?.trim()) { throw new InvalidRoomTypeDataError('Room type code is required'); } if (params.maxOccupancy < 1) { throw new InvalidRoomTypeDataError('Max occupancy must be at least 1'); } }} class StandardRoomTypeFactory extends RoomTypeFactory { createRoomType(params: RoomTypeParams): RoomType { this.validate(params); return new StandardRoomType(params); }} class SuiteRoomTypeFactory extends RoomTypeFactory { createRoomType(params: RoomTypeParams): RoomType { this.validate(params); // Suites have additional attributes return new SuiteRoomType({ ...params, hasSeparateLivingArea: true, includesButlerService: params.tier === 'LUXURY', }); }}The factory centralizes all creation logic: validation, pricing, inventory reservation, entity initialization. When you add a new booking channel, you add a method to the factory—all the complex creation logic is reused. This is far better than duplicating 100 lines of creation code in each controller.
The Problem:
Reservations have complex state-dependent behavior:
Using if/switch statements throughout the code leads to:
The State Pattern encapsulates state-specific behavior into dedicated classes.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
/** * ReservationState - abstract base for all states */abstract class ReservationState { protected context: Reservation; setContext(context: Reservation): void { this.context = context; } // State name for persistence and display abstract get name(): ReservationStatusName; // Default implementations throw - override in allowed states confirm(): void { throw new InvalidStateTransitionError( `Cannot confirm from ${this.name} state` ); } checkIn(roomIds: RoomId[]): void { throw new InvalidStateTransitionError( `Cannot check in from ${this.name} state` ); } checkOut(): void { throw new InvalidStateTransitionError( `Cannot check out from ${this.name} state` ); } cancel(reason: string): CancellationResult { throw new InvalidStateTransitionError( `Cannot cancel from ${this.name} state` ); } markNoShow(): void { throw new InvalidStateTransitionError( `Cannot mark no-show from ${this.name} state` ); } modify(changes: ReservationChanges): void { throw new InvalidStateTransitionError( `Cannot modify from ${this.name} state` ); } // Query methods - default implementations isModifiable(): boolean { return false; } isCancellable(): boolean { return false; } isActive(): boolean { return false; }} /** * PendingState - reservation created, awaiting payment */class PendingState extends ReservationState { get name(): ReservationStatusName { return 'PENDING'; } confirm(): void { // Validate payment received (external check) if (!this.context.hasValidPaymentGuarantee()) { throw new PaymentRequiredError( 'Cannot confirm without payment or guarantee' ); } // Record status change this.context.recordStatusChange( 'PENDING', 'CONFIRMED', 'Payment confirmed' ); // Transition to confirmed state this.context.transitionTo(new ConfirmedState()); // Emit domain event for notifications this.context.addDomainEvent(new ReservationConfirmedEvent( this.context.id, this.context.confirmationNumber, this.context.primaryGuestId )); } cancel(reason: string): CancellationResult { // Pending cancellation is simple - no penalties typically const result = new CancellationResult({ reservationId: this.context.id, cancelledAt: Instant.now(), penaltyAmount: Money.zero(), refundAmount: this.context.amountPaid, // Full refund reason, }); // Release inventory this.context.releaseInventory(); // Record and transition this.context.recordStatusChange('PENDING', 'CANCELLED', reason); this.context.transitionTo(new CancelledState()); this.context.addDomainEvent(new ReservationCancelledEvent( this.context.id, result )); return result; } modify(changes: ReservationChanges): void { // Pending reservations can be freely modified this.context.applyChanges(changes); } isModifiable(): boolean { return true; } isCancellable(): boolean { return true; }} /** * ConfirmedState - payment received, awaiting arrival */class ConfirmedState extends ReservationState { get name(): ReservationStatusName { return 'CONFIRMED'; } checkIn(roomIds: RoomId[]): void { // Validate room assignment if (roomIds.length !== this.context.roomCount) { throw new InvalidRoomAssignmentError( `Expected ${this.context.roomCount} rooms, got ${roomIds.length}` ); } // Validate rooms are available and correct type for (const roomId of roomIds) { this.context.validateRoomForAssignment(roomId); } // Assign rooms this.context.assignRooms(roomIds); // Record and transition this.context.recordStatusChange( 'CONFIRMED', 'CHECKED_IN', `Assigned to rooms: ${roomIds.join(', ')}` ); this.context.transitionTo(new CheckedInState()); this.context.addDomainEvent(new GuestCheckedInEvent( this.context.id, this.context.primaryGuestId, roomIds )); } cancel(reason: string): CancellationResult { // Apply cancellation policy const penalty = this.context.calculateCancellationPenalty(); const refund = this.context.amountPaid.subtract(penalty); const result = new CancellationResult({ reservationId: this.context.id, cancelledAt: Instant.now(), penaltyAmount: penalty, refundAmount: refund, reason, policyApplied: this.context.cancellationPolicy.name, }); // Release inventory this.context.releaseInventory(); // Process refund if applicable if (refund.isPositive()) { this.context.initiateRefund(refund); } // Record and transition this.context.recordStatusChange('CONFIRMED', 'CANCELLED', reason); this.context.transitionTo(new CancelledState()); this.context.addDomainEvent(new ReservationCancelledEvent( this.context.id, result )); return result; } markNoShow(): void { // Charge no-show fee (typically first night) const noShowPenalty = this.context.calculateNoShowPenalty(); this.context.chargeNoShowFee(noShowPenalty); // Release inventory for remaining nights this.context.releaseInventory(); // Record and transition this.context.recordStatusChange( 'CONFIRMED', 'NO_SHOW', `Guest did not arrive. Penalty: ${noShowPenalty}` ); this.context.transitionTo(new NoShowState()); this.context.addDomainEvent(new GuestNoShowEvent( this.context.id, noShowPenalty )); } modify(changes: ReservationChanges): void { // Confirmed can be modified with policy checks this.context.validateModificationAllowed(changes); this.context.applyChanges(changes); } isModifiable(): boolean { return true; } isCancellable(): boolean { return true; } isActive(): boolean { return true; }} /** * CheckedInState - guest is currently staying */class CheckedInState extends ReservationState { get name(): ReservationStatusName { return 'CHECKED_IN'; } checkOut(): void { // Validate no outstanding balance if (this.context.hasOutstandingBalance()) { throw new OutstandingBalanceError( `Cannot check out with balance: ${this.context.outstandingBalance}` ); } // Release rooms this.context.releaseAssignedRooms(); // Record guest stay for loyalty/history this.context.recordCompletedStay(); // Record and transition this.context.recordStatusChange( 'CHECKED_IN', 'CHECKED_OUT', 'Guest departed' ); this.context.transitionTo(new CheckedOutState()); this.context.addDomainEvent(new GuestCheckedOutEvent( this.context.id, this.context.primaryGuestId, this.context.totalCharged )); } // Special modification: early departure modify(changes: ReservationChanges): void { if (changes.shortenStay) { // Update checkout to today this.context.updateCheckOutDate(LocalDate.now()); this.context.recalculatePricing(); } else { throw new InvalidModificationError( 'Only early departure allowed for checked-in guests' ); } } isActive(): boolean { return true; }} /** * Terminal states - no further transitions */class CancelledState extends ReservationState { get name(): ReservationStatusName { return 'CANCELLED'; } // All operations throw default errors - nothing allowed} class NoShowState extends ReservationState { get name(): ReservationStatusName { return 'NO_SHOW'; }} class CheckedOutState extends ReservationState { get name(): ReservationStatusName { return 'CHECKED_OUT'; }} /** * Reservation uses state pattern internally */class Reservation { private state: ReservationState; constructor(params: ReservationParams) { // ... initialization this.transitionTo(new PendingState()); } transitionTo(state: ReservationState): void { this.state = state; this.state.setContext(this); } // Delegate all state-dependent operations confirm(): void { this.state.confirm(); } checkIn(roomIds: RoomId[]): void { this.state.checkIn(roomIds); } checkOut(): void { this.state.checkOut(); } cancel(reason: string): CancellationResult { return this.state.cancel(reason); } isModifiable(): boolean { return this.state.isModifiable(); } // ... other delegated methods}The State pattern adds classes but removes conditionals. Each state class is small and focused. Adding a new state (e.g., 'ON_HOLD') requires creating one class and updating allowed transitions—no hunting through if/else chains. The explicit state machine also serves as documentation.
The Problem:
Many calculations share a common structure but differ in specific steps:
The Template Method Pattern defines this skeleton in a base class, allowing subclasses to override specific steps without changing the overall structure.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
/** * Abstract rate calculator using Template Method pattern */abstract class RateCalculator { /** * Template method - defines the algorithm skeleton * Subclasses override specific steps, not this method */ calculateNightlyRate( roomType: RoomType, date: LocalDate, context: RateContext ): NightlyRate { // Step 1: Get base rate (may vary by subclass) const baseRate = this.getBaseRate(roomType, date); // Step 2: Apply seasonal modification const afterSeason = this.applySeasonalModifier(baseRate, date); // Step 3: Apply day-of-week modification const afterDayOfWeek = this.applyDayOfWeekModifier(afterSeason, date); // Step 4: Apply demand-based adjustment (hook method) const afterDemand = this.applyDemandModifier(afterDayOfWeek, date, context); // Step 5: Apply rate plan discount const afterRatePlan = this.applyRatePlanModifier(afterDemand, context); // Step 6: Calculate extras (extra person charges, etc.) const extras = this.calculateExtras(roomType, context); // Step 7: Build result return this.buildNightlyRate( date, baseRate, afterRatePlan, extras, this.getAppliedModifiers() ); } // ============ Required abstract methods (subclasses must implement) ============ protected abstract getBaseRate(roomType: RoomType, date: LocalDate): Money; protected abstract applySeasonalModifier(rate: Money, date: LocalDate): Money; // ============ Hook methods (subclasses may override) ============ protected applyDayOfWeekModifier(rate: Money, date: LocalDate): Money { // Default: no day-of-week adjustment return rate; } protected applyDemandModifier( rate: Money, date: LocalDate, context: RateContext ): Money { // Default: no demand adjustment (override for dynamic pricing) return rate; } protected applyRatePlanModifier(rate: Money, context: RateContext): Money { if (!context.ratePlan) return rate; return context.ratePlan.applyToBaseRate(rate); } protected calculateExtras( roomType: RoomType, context: RateContext ): Money { // Default: calculate extra person charges return roomType.calculateExtraPersonCharge( context.adults, context.children, context.extraPersonRates ); } protected buildNightlyRate( date: LocalDate, baseRate: Money, adjustedRate: Money, extras: Money, modifiers: AppliedModifier[] ): NightlyRate { return new NightlyRate({ date, baseRate, adjustedRate, extraPersonCharge: extras, totalBeforeTax: adjustedRate.add(extras), modifiersApplied: modifiers, }); } protected getAppliedModifiers(): AppliedModifier[] { return []; // Subclasses track their own modifiers }} /** * Standard rate calculator - uses configured rate tables */class StandardRateCalculator extends RateCalculator { private readonly rateRepository: RateRepository; private appliedModifiers: AppliedModifier[] = []; constructor(rateRepository: RateRepository) { super(); this.rateRepository = rateRepository; } protected getBaseRate(roomType: RoomType, date: LocalDate): Money { // Look up configured rate, fall back to rack rate const configuredRate = this.rateRepository.getBaseRate( roomType.id, date ); return configuredRate ?? roomType.rackRate; } protected applySeasonalModifier(rate: Money, date: LocalDate): Money { const modifier = this.rateRepository.getSeasonalModifier(date); if (modifier.isIdentity()) return rate; this.appliedModifiers.push({ type: 'SEASONAL', description: modifier.description, impact: modifier.value, }); return modifier.apply(rate); } protected applyDayOfWeekModifier(rate: Money, date: LocalDate): Money { const dow = date.dayOfWeek; if (dow === DayOfWeek.SATURDAY || dow === DayOfWeek.FRIDAY) { const weekendPremium = rate.multiply(0.15); // 15% premium this.appliedModifiers.push({ type: 'DAY_OF_WEEK', description: 'Weekend premium', impact: 15, }); return rate.add(weekendPremium); } return rate; } protected getAppliedModifiers(): AppliedModifier[] { return [...this.appliedModifiers]; }} /** * Dynamic rate calculator - includes demand-based pricing */class DynamicRateCalculator extends StandardRateCalculator { private readonly demandAnalyzer: DemandAnalyzer; protected applyDemandModifier( rate: Money, date: LocalDate, context: RateContext ): Money { const occupancyForecast = this.demandAnalyzer.getOccupancyForecast( context.hotelId, date ); // Increase prices as occupancy rises let multiplier = 1.0; if (occupancyForecast > 0.9) { multiplier = 1.25; // 25% premium at 90%+ occupancy } else if (occupancyForecast > 0.75) { multiplier = 1.10; // 10% premium at 75%+ occupancy } else if (occupancyForecast < 0.4) { multiplier = 0.90; // 10% discount below 40% } if (multiplier !== 1.0) { this.appliedModifiers.push({ type: 'DEMAND', description: `Demand adjustment (${Math.round(occupancyForecast * 100)}% occupancy)`, impact: (multiplier - 1) * 100, }); } return rate.multiply(multiplier); }} /** * Template method for cancellation penalty calculation */abstract class CancellationCalculator { /** * Template method for penalty calculation */ calculatePenalty( reservation: Reservation, cancellationTime: Instant ): CancellationPenalty { // Step 1: Calculate hours until check-in const hoursUntilCheckin = this.calculateHoursUntilCheckin( reservation, cancellationTime ); // Step 2: Find applicable policy tier const tier = this.findApplicableTier( reservation.cancellationPolicy, hoursUntilCheckin ); // Step 3: Calculate base penalty const basePenalty = this.calculateBasePenalty(reservation, tier); // Step 4: Apply any adjustments (loyalty, compensation, etc.) const adjustedPenalty = this.applyAdjustments( basePenalty, reservation ); // Step 5: Build result return this.buildPenaltyResult( reservation, tier, basePenalty, adjustedPenalty ); } // Required implementations protected abstract calculateHoursUntilCheckin( reservation: Reservation, cancellationTime: Instant ): number; protected abstract calculateBasePenalty( reservation: Reservation, tier: CancellationTier ): Money; // Hook methods with defaults protected findApplicableTier( policy: CancellationPolicy, hoursUntilCheckin: number ): CancellationTier { // Find highest tier where we're past the threshold return policy.tiers .filter(t => hoursUntilCheckin <= t.hoursBeforeCheckin) .sort((a, b) => a.hoursBeforeCheckin - b.hoursBeforeCheckin)[0] ?? policy.defaultTier; } protected applyAdjustments( penalty: Money, reservation: Reservation ): Money { // Default: no adjustments return penalty; } protected buildPenaltyResult( reservation: Reservation, tier: CancellationTier, basePenalty: Money, adjustedPenalty: Money ): CancellationPenalty { return new CancellationPenalty({ reservationId: reservation.id, tierApplied: tier, basePenalty, adjustedPenalty, refundAmount: reservation.totalPaid.subtract(adjustedPenalty), }); }} /** * Standard implementation of cancellation calculator */class StandardCancellationCalculator extends CancellationCalculator { protected calculateHoursUntilCheckin( reservation: Reservation, cancellationTime: Instant ): number { // Use hotel's check-in time (typically 3 PM) const checkinInstant = reservation.checkInDate .atTime(reservation.hotel.checkInTime) .atZone(reservation.hotel.timezone) .toInstant(); return cancellationTime.until(checkinInstant, ChronoUnit.HOURS); } protected calculateBasePenalty( reservation: Reservation, tier: CancellationTier ): Money { if (tier.fixedPenalty) { return tier.fixedPenalty; } if (tier.nightsCharged) { // Charge N nights const nightlyCharges = reservation.roomCharges .slice(0, tier.nightsCharged); return nightlyCharges.reduce( (sum, charge) => sum.add(charge.amount), Money.zero() ); } // Percentage of total return reservation.totalAmount.multiply(tier.penaltyPercentage / 100); } protected applyAdjustments( penalty: Money, reservation: Reservation ): Money { // Elite loyalty members get reduced penalties const guest = reservation.primaryGuest; if (guest.loyaltyTier === LoyaltyTier.DIAMOND) { return penalty.multiply(0.5); // 50% reduction } if (guest.loyaltyTier === LoyaltyTier.PLATINUM) { return penalty.multiply(0.75); // 25% reduction } return penalty; }}Template Method uses inheritance (algorithm skeleton in base class). Strategy uses composition (swap entire algorithms). Use Template Method when algorithms share significant structure; use Strategy when algorithms are completely different. They can be combined: StandardRateCalculator uses Template Method internally but could be injected as a Strategy.
Real systems rarely use patterns in isolation. The Hotel Booking System demonstrates how patterns reinforce each other:
| Pattern Combination | Where Used | Benefit |
|---|---|---|
| Factory + State | ReservationFactory creates Reservation in PendingState | Consistent initial state, encapsulated creation |
| State + Template Method | Each state uses template for transition validation | Consistent transition logic, extensible states |
| Factory + Template Method | RoomTypeFactory uses template for validation | Shared validation, specific creation |
| State + Observer | State transitions emit domain events | Decoupled notification, audit logging |
| Template + Strategy | Rate calculator template uses pricing strategies | Flexible algorithms within consistent structure |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
/** * Combining Factory, State, and Template Method */class BookingService { private readonly reservationFactory: ReservationFactory; private readonly rateCalculator: RateCalculator; // Template Method private readonly eventBus: EventBus; async createBooking(input: BookingInput): Promise<BookingResult> { // Factory creates reservation with proper initial state const reservation = await this.reservationFactory.create(input); // State pattern handles confirmation (if payment ready) if (input.paymentInfo) { await this.processPayment(reservation, input.paymentInfo); reservation.confirm(); // State transition } // Save and return await this.reservationRepository.save(reservation); // State transitions emit events via Observer pattern this.eventBus.publishAll(reservation.domainEvents); return new BookingResult({ reservation, confirmationNumber: reservation.confirmationNumber, pricing: reservation.pricingDetails, }); } async modifyBooking( reservationId: ReservationId, changes: BookingChanges ): Promise<ModificationResult> { const reservation = await this.reservationRepository.findById(reservationId); // State pattern enforces modification rules if (!reservation.isModifiable()) { throw new ModificationNotAllowedError( `Cannot modify reservation in ${reservation.statusName} state` ); } // Calculate repricing using Template Method const newPricing = this.rateCalculator.recalculate( reservation, changes ); // State-aware modification reservation.modify({ ...changes, newPricing, }); await this.reservationRepository.save(reservation); this.eventBus.publishAll(reservation.domainEvents); return new ModificationResult({ reservation, pricingDifference: newPricing.difference, }); }}We've explored how three fundamental patterns—Factory, State, and Template Method—provide structure and extensibility to the Hotel Booking System.
What's Next:
With patterns applied, we're ready for the Complete Design Walkthrough—a comprehensive end-to-end trace through the system showing how all components work together to handle a real booking scenario.
You now understand how to apply Factory, State, and Template Method patterns in a hotel booking context. These patterns aren't theoretical—they solve real problems of complex object creation, lifecycle management, and algorithm standardization. Use them when you face similar problems.