Building a Robust Reservation System with Event-Driven Architecture

In this post, I’ll guide you through creating a scalable reservation system using event-driven architecture. We’ll explore domain modeling, event handling, and message processing - essential concepts for modern distributed systems.

The Reservation Process Flow

Let’s start by visualizing the reservation workflow with a Mermaid diagram:

flowchart TD A[Start Reservation Process] --> B[Receive Reservation Request] B --> C{Validate Request} C -->|Valid| D[Check Availability] C -->|Invalid| E[Return Error to Client] D --> F{Availability?} F -->|Available| G[Create Reservation] F -->|Not Available| H[Notify Client of Unavailability] G --> I[Send Confirmation via Wolverine] H --> I I --> J[Store Reservation in Postgres] J --> K[Return Success Response to Client] E --> L[End Process] K --> L

This diagram shows the end-to-end process from receiving a request to returning a response, with key decision points and actions along the way.

Domain Model for Reservations

A well-designed domain model is crucial for representing our business entities and their relationships:

public class Reservation
{
    public Guid ReservationId { get; set; }
    public DateTime ReservationDate { get; set; }
    public DateTime CreatedAt { get; set; }
    public Customer Customer { get; set; }
    public Resource Resource { get; set; }
    public ReservationStatus Status { get; set; }
}

public class Customer
{
    public Guid CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

public class Resource
{
    public Guid ResourceId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public ResourceType Type { get; set; }
}

public enum ReservationStatus
{
    Pending,
    Confirmed,
    Cancelled
}

public enum ResourceType
{
    Room,
    Equipment,
    Venue
}

This model captures the core entities: Reservations, Customers, and Resources, along with their properties and relationships.

Implementing Domain Events

For an event-driven architecture, we need to define events that represent significant system occurrences:

public interface IDomainEvent
{
    DateTime OccurredOn { get; }
}

public class ReservationCreatedEvent : IDomainEvent
{
    public Guid ReservationId { get; }
    public DateTime ReservationDate { get; }
    public Guid CustomerId { get; }
    public Guid ResourceId { get; }

    public DateTime OccurredOn { get; } = DateTime.UtcNow;

    public ReservationCreatedEvent(Guid reservationId, DateTime reservationDate, Guid customerId, Guid resourceId)
    {
        ReservationId = reservationId;
        ReservationDate = reservationDate;
        CustomerId = customerId;
        ResourceId = resourceId;
    }
}

public class ReservationCancelledEvent : IDomainEvent
{
    public Guid ReservationId { get; }
    public Guid CustomerId { get; }

    public DateTime OccurredOn { get; } = DateTime.UtcNow;

    public ReservationCancelledEvent(Guid reservationId, Guid customerId)
    {
        ReservationId = reservationId;
        CustomerId = customerId;
    }
}

Enhancing the Domain Model with Event Publishing

Let’s modify our Reservation class to publish domain events:

public class Reservation
{
    public Guid ReservationId { get; private set; }
    public DateTime ReservationDate { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public Customer Customer { get; private set; }
    public Resource Resource { get; private set; }
    public ReservationStatus Status { get; private set; }

    private readonly List<IDomainEvent> _events = new List<IDomainEvent>();
    public IReadOnlyCollection<IDomainEvent> Events => _events.AsReadOnly();

    public Reservation(Guid reservationId, DateTime reservationDate, Customer customer, Resource resource)
    {
        ReservationId = reservationId;
        ReservationDate = reservationDate;
        CreatedAt = DateTime.UtcNow;
        Customer = customer;
        Resource = resource;
        Status = ReservationStatus.Pending;

        _events.Add(new ReservationCreatedEvent(ReservationId, ReservationDate, Customer.CustomerId, Resource.ResourceId));
    }

    public void Cancel()
    {
        Status = ReservationStatus.Cancelled;
        _events.Add(new ReservationCancelledEvent(ReservationId, Customer.CustomerId));
    }
}

This implementation tracks domain events for later processing by event handlers.

Creating a Message Envelope Pattern

To properly transmit events across system boundaries, we’ll implement the Message Envelope pattern:

public class MessageEnvelope<T>
{
    public Guid MessageId { get; private set; }
    public DateTime OccurredOn { get; private set; }
    public string MessageType { get; private set; }
    public T Payload { get; private set; }
    public Dictionary<string, string> Metadata { get; private set; }

    public MessageEnvelope(T payload, string messageType)
    {
        MessageId = Guid.NewGuid();
        OccurredOn = DateTime.UtcNow;
        MessageType = messageType;
        Payload = payload;
        Metadata = new Dictionary<string, string>();
    }

    public void AddMetadata(string key, string value)
    {
        Metadata[key] = value;
    }
}

Adding Serialization Support

For transmitting messages between services, we need serialization capabilities:

using System.Text.Json;

public class MessageEnvelope<T>
{
    // Previous properties and constructor...

    // Serialize to JSON
    public string Serialize()
    {
        return JsonSerializer.Serialize(this);
    }

    // Deserialize from JSON
    public static MessageEnvelope<T> Deserialize(string json)
    {
        return JsonSerializer.Deserialize<MessageEnvelope<T>>(json);
    }
}

Usage Example

Here’s how you might use these components in practice:

// Create a reservation event
var reservationEvent = new ReservationCreatedEvent(
    Guid.NewGuid(), 
    DateTime.UtcNow, 
    Guid.NewGuid(), 
    Guid.NewGuid()
);

// Wrap it in an envelope with metadata
var envelope = new MessageEnvelope<ReservationCreatedEvent>(
    reservationEvent, 
    nameof(ReservationCreatedEvent)
);
envelope.AddMetadata("CorrelationId", "12345");
envelope.AddMetadata("Source", "ReservationSystem");

// Serialize for transmission
string json = envelope.Serialize();

// Later, deserialize
var deserializedEnvelope = MessageEnvelope<ReservationCreatedEvent>.Deserialize(json);

Benefits of This Approach

  1. Loose coupling - Services communicate through events rather than direct calls
  2. Scalability - Components can be scaled independently
  3. Resilience - System remains operational even if some services are temporarily unavailable
  4. Auditability - Events provide a complete history of system activities
  5. Flexibility - Easy to extend with new event types and handlers

In this post, we’ve covered the essential building blocks for a robust, event-driven reservation system. This architecture provides a solid foundation that can evolve to meet changing business requirements while maintaining system integrity.

For more details on specific aspects of this architecture, check out these related posts:

  1. Reservation System Component Diagram
  2. Reservation System Activity Diagram
  3. Reservation System Sequence Diagram

Related Posts