Conference Session Booking System

Architectural Brief

1. Executive Summary

This document outlines the architecture for an event-sourced system using Marten and Redis that handles conference session bookings with waitlists. The architecture supports multiple front-end application servers communicating with single instances of PostgreSQL and Redis servers, focusing on scalability, consistency, and user experience.

System Capabilities
  • Sign up for seat-limited sessions
  • Automatic waitlist placement when sessions are full
  • Automatic promotion from waitlists when seats become available

2. System Architecture Overview

The system follows a CQRS (Command Query Responsibility Segregation) and Event Sourcing architecture pattern, leveraging:

  • Marten For event sourcing and read model projections
  • Redis For real-time availability, distributed locking, and waitlist management
  • ASP.NET Core For web application servers with Razor Pages
  • C# Using functional programming principles including immutable record types
Key Benefits
  • Complete audit trail with event sourcing
  • Real-time state management with Redis
  • Scalable across multiple application servers
  • Race-condition prevention with distributed locking
System Architecture Overview

2.1 Key Components

Multiple ASP.NET Core applications with Razor Pages, jQuery, and Bootstrap

Command handlers, event handlers, query handlers, and background services

Marten for event sourcing and document DB, Redis for distributed cache

PostgreSQL for event stream and projections

3. Domain Model

The domain model uses functional programming principles with immutable record types:

// Core domain entities as immutable records
public record Session(Guid Id, string Title, int TotalSeats, DateTimeOffset StartTime);
public record Reservation(Guid Id, Guid SessionId, Guid UserId, DateTimeOffset CreatedAt);
public record WaitlistEntry(Guid Id, Guid SessionId, Guid UserId, int Position, DateTimeOffset JoinedAt);

4. Command Model

Commands represent user intentions and are processed by command handlers:

Command Description Properties
CreateSession Create a conference session Id, Title, Description, Speaker, TotalSeats, DateTime
TryReserveSessionSeat Attempt to reserve a seat or join waitlist SessionId, UserId
CancelReservation Cancel an existing reservation ReservationId, UserId
LeaveWaitlist Leave a session waitlist SessionId, UserId
PromoteFromWaitlist Promote user from waitlist to reservation WaitlistEntryId, SessionId, UserId

5. Event Model

Events represent facts that have occurred and are stored in Marten's event store:

Event Description Properties
SessionCreated New session created Id, Title, Description, Speaker, TotalSeats, DateTime
SessionSeatReserved Seat successfully reserved ReservationId, SessionId, UserId, Timestamp
SessionWaitlistAppended User added to waitlist WaitlistEntryId, SessionId, UserId, Position, Timestamp
ReservationCancelled Reservation cancelled ReservationId, SessionId, UserId, Timestamp
WaitlistLeft User removed from waitlist WaitlistEntryId, SessionId, UserId, Timestamp
WaitlistUserPromoted User promoted from waitlist WaitlistEntryId, SessionId, UserId, ReservationId, Timestamp

6. Projections

Marten projections will maintain read models for efficient querying:

Projection Purpose Source Events
SessionDetails Current session information SessionCreated, SessionUpdated
SessionAvailability Seats total/available/reserved SessionCreated, SessionSeatReserved, ReservationCancelled, WaitlistUserPromoted
UserReservations List of a user's reservations SessionSeatReserved, ReservationCancelled, WaitlistUserPromoted
SessionWaitlist Prioritized waitlist for a session SessionWaitlistAppended, WaitlistLeft, WaitlistUserPromoted

7. Event Sourcing Flow

The system implements a standard event sourcing pattern:

Event Sourcing Flow
  1. Commands are validated
  2. Command handlers update aggregates
  3. Aggregates emit events
  4. Events are stored in the event stream
  5. Events are published to event handlers
  6. Event handlers update projections and Redis
  7. Queries read from projections or Redis

8. Reservation Process Flow

The reservation process implements a single-command approach that handles both successful reservations and waitlist additions:

Reservation Process Flow
Waitlist Promotion Flow

8.1 Happy Path (Seat Available)

Process Steps
  1. User requests to attend a session
  2. Web app performs a quick availability check (UI only)
  3. Web app submits TryReserveSessionSeatCommand
  4. Command handler acquires a distributed lock
  5. Command handler verifies seat availability
  6. If seats are available, a SessionSeatReservedEvent is stored
  7. Event handler updates Redis cache
  8. User receives confirmation

8.2 Sad Path (Session Full)

Process Steps
  1. User requests to attend a session
  2. Web app submits TryReserveSessionSeatCommand
  3. Command handler acquires a distributed lock
  4. Command handler finds no seats available
  5. A SessionWaitlistAppendedEvent is stored
  6. Event handler updates waitlist in Redis
  7. User receives waitlist confirmation

8.3 Waitlist Promotion

Process Steps
  1. User cancels reservation
  2. CancelReservationCommand is submitted
  3. ReservationCancelledEvent is stored
  4. Event handler retrieves next waitlisted user from Redis
  5. PromoteFromWaitlistCommand is submitted
  6. WaitlistUserPromotedEvent is stored
  7. Redis is updated to reflect the promotion

9. Redis Usage Strategy

Redis will be used for:

Real-time availability cache

Store current seat counts for fast reads

session:{sessionId}:available → int
session:{sessionId}:total → int
Distributed locking

Prevent race conditions when booking last seats

lock:session:{sessionId} → lockToken
Sorted waitlists

Maintain position-based waitlists using Redis Sorted Sets

waitlist:{sessionId} → {userId: position}
Pub/Sub notifications

Broadcast availability changes to all app servers

PUBLISH session_update:{sessionId} {JSON data}

10. Implementation Considerations

Service Initialization

On application startup, these initialization steps are necessary:

Projection synchronization
  • Ensure Marten projections are up-to-date with event store
Redis initialization
// Prime Redis cache from projections
foreach (var session in await sessionAvailabilityRepository.GetAllAsync())
{
    await redis.StringSetAsync($"session:{session.Id}:available", session.AvailableSeats);
    await redis.StringSetAsync($"session:{session.Id}:total", session.TotalSeats);
}

// Prime waitlists in Redis
foreach (var waitlist in await waitlistRepository.GetAllAsync())
{
    foreach (var entry in waitlist.Entries)
    {
        await redis.SortedSetAddAsync(
            $"waitlist:{waitlist.SessionId}", 
            entry.UserId.ToString(), 
            entry.Position);
    }
}

Concurrency and Race Condition Handling

To handle concurrent operations:

Redis distributed locks
var lockKey = $"lock:session:{sessionId}";
var lockToken = Guid.NewGuid().ToString();

if (await redis.LockTakeAsync(lockKey, lockToken, TimeSpan.FromSeconds(10)))
{
    try
    {
        // Process reservation logic
    }
    finally
    {
        await redis.LockReleaseAsync(lockKey, lockToken);
    }
}
Marten's optimistic concurrency control

Marten provides built-in optimistic concurrency control for event streams to prevent conflicts when multiple users update the same aggregate.

Scalability Considerations

Application server scaling
  • Multiple identical ASP.NET Core instances can be deployed
  • No state is maintained in the application servers
Redis scaling
  • Consider Redis clustering for higher throughput scenarios
  • Use Redis read replicas for read-heavy workloads
Postgres (Marten) scaling
  • Separate read/write connections
  • Consider read replicas for projections

11. Technology Stack Summary

Backend

  • .NET 8
  • ASP.NET Core with Razor Pages
  • C# with functional programming patterns
  • Marten for event sourcing and document DB
  • StackExchange.Redis client library

Frontend

  • Bootstrap for responsive UI
  • jQuery for client interactions
  • SignalR for real-time waitlist updates

Infrastructure

  • PostgreSQL (for Marten)
  • Redis
  • Docker Compose or Kubernetes for deployment

12. Conclusion

This architecture leverages event sourcing with Marten to maintain a reliable source of truth while using Redis to handle the real-time aspects of seat availability and waitlist management. The unified TryReserveSessionSeatCommand approach provides a clean user experience by handling both reservation and waitlist scenarios in a single flow, eliminating race conditions and providing immediate feedback to users.

The system is designed to be scalable across multiple application servers while maintaining data consistency through appropriate locking mechanisms and event-sourced state management.