Architectural Brief
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.
The system follows a CQRS (Command Query Responsibility Segregation) and Event Sourcing architecture pattern, leveraging:
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);
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 |
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 |
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 |
The system implements a standard event sourcing pattern:
The reservation process implements a single-command approach that handles both successful reservations and waitlist additions:
Redis will be used for:
Store current seat counts for fast reads
session:{sessionId}:available → int
session:{sessionId}:total → int
Prevent race conditions when booking last seats
lock:session:{sessionId} → lockToken
Maintain position-based waitlists using Redis Sorted Sets
waitlist:{sessionId} → {userId: position}
Broadcast availability changes to all app servers
PUBLISH session_update:{sessionId} {JSON data}
On application startup, these initialization steps are necessary:
// 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);
}
}
To handle concurrent operations:
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 provides built-in optimistic concurrency control for event streams to prevent conflicts when multiple users update the same aggregate.
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.